Vectorisation and performance

R · Advanced · 14 min

Slow R code is usually written in a few avoidable patterns that are easy to fix once spotted.

R's interpreter is slow per operation but its vectorised primitives run in compiled C. Fast R therefore pushes work into as few, as large operations as possible.

Measure first. system.time(expr) times one expression; bench::mark() compares alternatives and checks they give the same result; profvis::profvis() shows where the time goes in a larger piece of code.

The common fixes, roughly in order of payoff:

  • Vectorise: x[x > 0] <- 0 instead of a loop with if; rowSums and colMeans instead of apply; cumsum, diff, pmin/pmax, ifelse, match and %in% instead of hand-written loops.
  • Preallocate: growing a vector with out <- c(out, x) or a list with out[[length(out) + 1]] copies the whole object each time, making the loop quadratic. Create numeric(n) or vector("list", n) up front.
  • Avoid repeated work in loops: hoist invariant calculations, and do rbind once at the end with do.call(rbind, pieces).
  • Choose the right tool: matrices for homogeneous numeric data, data.table for big tabular work, Rcpp to write a hot loop in C++.

Parallelism (parallel::mclapply, the future package) helps CPU-bound independent tasks but adds overhead; optimise the serial code first.

Example

n <- 1e5
x <- runif(n)

grow <- function(x) { out <- c(); for (v in x) out <- c(out, v * 2); out }
prealloc <- function(x) { out <- numeric(length(x))
  for (i in seq_along(x)) out[i] <- x[i] * 2; out }
vectorised <- function(x) x * 2

t_grow <- system.time(a <- grow(x[1:20000]))[["elapsed"]]
t_pre  <- system.time(b <- prealloc(x[1:20000]))[["elapsed"]]
t_vec  <- system.time(for (k in 1:100) c <- vectorised(x))[["elapsed"]] / 100
print(identical(a, b))                    #> [1] TRUE
print(t_grow > t_pre)                     #> [1] TRUE (quadratic copying)
cat(sprintf("grow %.3fs  prealloc %.4fs  vectorised %.5fs (on 5x more data)\n",
            t_grow, t_pre, t_vec))

m <- matrix(runif(1e6), ncol = 100)
print(all.equal(rowSums(m), apply(m, 1, sum)))   #> [1] TRUE (rowSums is faster)

Remember

  • Measure with system.time, bench::mark or profvis first
  • Vectorised primitives run in C: use them instead of loops
  • Preallocate results; never grow vectors in a loop
  • Reach for data.table or Rcpp when base R isn't enough

Watch out for

  • Growing a data frame with rbind inside a loop, which copies everything every iteration.

Check yourself

  1. Why is out <- c(out, v) inside a loop slow for large inputs?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: cran.r-project.org/doc/manuals/r-release/R-exts.html#Tidying-and-profiling-R-code

Builds on

Unlocks

Nothing builds on this yet.

Vectorisation and performance · R · Topolearn