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] <- 0instead of a loop withif;rowSumsandcolMeansinstead ofapply;cumsum,diff,pmin/pmax,ifelse,matchand%in%instead of hand-written loops. - Preallocate: growing a vector with
out <- c(out, x)or a list without[[length(out) + 1]]copies the whole object each time, making the loop quadratic. Createnumeric(n)orvector("list", n)up front. - Avoid repeated work in loops: hoist invariant calculations, and do
rbindonce at the end withdo.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::markorprofvisfirst - 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.