Skip to content
AgileLogics
6 min read

Performance and Profiling

Prerequisites: Loops, The apply family

The first rule of performance work: measure before you change anything. Most R code is fast enough; when it isn't, the slow part is usually a few specific lines.

Measuring

x <- runif(1e6)

loop_sum <- function(v) {
  total <- 0
  for (value in v) total <- total + value
  total
}

t_loop <- system.time(loop_sum(x))["elapsed"]
t_vec <- system.time(sum(x))["elapsed"]
t_vec <= t_loop
elapsed
   TRUE

system.time() times an expression. For reliable comparisons of small pieces of code, the bench package runs each many times: bench::mark(loop_sum(x), sum(x)). To find where a long script spends its time, profvis (profvis::profvis({ ... })) shows an interactive breakdown by line.

The big wins

1. Vectorize. Built-in vectorized functions (sum, cumsum, ifelse, paste, rowSums, arithmetic) run in fast compiled code. A vectorized version is often 10–100× faster than an R loop.

2. Don't grow objects in a loop. Appending (result <- c(result, x)) copies the whole vector every time. Preallocate instead:

n <- 5
squares <- numeric(n)
for (i in seq_len(n)) squares[i] <- i^2
squares
[1]  1  4  9 16 25

3. Use the right tool for big data.

  • data.table — extremely fast grouping, joining, and reading of large files (fread), with a concise syntax: dt[, .(avg = mean(salary)), by = team].
  • arrow — reads Parquet files and datasets larger than memory, working with dplyr verbs.
  • duckdb — an in-process analytical database that runs dplyr or SQL on large files very fast.

4. Move hot loops to C++. When a loop truly can't be vectorized, the Rcpp package lets you write that function in C++ and call it from R.

5. Run in parallel. Independent tasks (simulations, model fits per group) can use several CPU cores — for example with the future and furrr packages: furrr::future_map(...) is a parallel map().

Memory

object.size(x) shows how much memory an object uses. R copies objects when they're modified (copy-on-modify), so very large data frames that are changed repeatedly can use a lot of memory — another case where data.table (which modifies in place) or a database helps.

Common mistakes

  • Optimizing code that isn't the bottleneck. Profile first.
  • Rewriting readable code for tiny gains. Fast enough and clear beats fastest and unreadable.