SyntaxStudy
Sign Up
R Vectorised Alternatives and Performance Considerations
R Beginner 1 min read

Vectorised Alternatives and Performance Considerations

One of R's core design principles is that loops should often be replaced by vectorised operations. When you apply an arithmetic operator or a function to a vector, R calls optimised C or Fortran routines internally, which are orders of magnitude faster than equivalent R-level for loops. Understanding which operations are already vectorised — and writing your own vectorised functions using ifelse(), Vectorize(), and the apply family — is key to writing performant R code. The Vectorize() function wraps a scalar function into a vectorised one by calling mapply() internally, but this is a convenience wrapper, not a true low-level vectorisation. For true performance-critical code, the Rcpp package allows you to write compiled C++ functions that integrate seamlessly with R. The microbenchmark and bench packages measure execution time precisely, allowing you to compare the performance of different implementations. When loops are unavoidable — for instance, in iterative algorithms where each step depends on the previous — several practices minimise their cost: pre-allocate result vectors to the correct size, avoid growing objects dynamically, use seq_along() instead of 1:length(x) to avoid the 1:0 pitfall when a vector is empty, and consider whether the loop body can itself be vectorised. The compiler package and byte-compilation (enabled by default in modern R) can also provide moderate speed improvements for loop-heavy code.
Example
# Vectorised vs loop: summing squares
n <- 1e6
x <- seq_len(n)

# Loop version (slow)
loop_sum <- function(x) {
    total <- 0
    for (val in x) total <- total + val^2
    total
}

# Vectorised version (fast)
vec_sum <- function(x) sum(x^2)

# Verify they match
loop_sum(1:10) == vec_sum(1:10)   # TRUE

# seq_along() — safe loop index (handles empty vectors)
items <- c("a", "b", "c")
for (i in seq_along(items)) {
    cat(i, "->", items[i], "\n")
}

# The 1:length(x) pitfall
empty <- c()
# 1:length(empty)   # gives 1 0 — iterates TWICE! Wrong.
seq_along(empty)    # integer(0) — correct, zero iterations

# Vectorize() wrapper
scalar_add <- function(x, y) {
    if (x > 0) x + y else y
}
vec_add <- Vectorize(scalar_add)
vec_add(c(-1, 2, -3, 4), c(10, 20, 30, 40))   # 10 22 30 44

# Pre-allocation benchmark illustration
slow_grow <- function(n) {
    result <- c()
    for (i in seq_len(n)) result <- c(result, i^2)
    result
}
fast_pre <- function(n) {
    result <- numeric(n)
    for (i in seq_len(n)) result[i] <- i^2
    result
}
# fast_pre is typically 10-100x faster for large n

# Functional replacement for a common loop pattern
# Instead of:  for (i in 1:n) result[i] <- f(x[i])
# Prefer:      result <- sapply(x, f)
# Or:          result <- f(x)   when f is already vectorised