Skip to content
Performance and Profiling
step 1/4

Reading — step 1 of 4

Learn

~2 min readMetaprogramming, Errors, Performance

R is interpreted and dynamic — slow by default. But the parts that matter (linear algebra, summary stats) call optimized C/Fortran. The art is keeping your hot path in those primitives.

Profile before optimizing. Rprof is built in:

Rprof("profile.out")
slow_function()
Rprof(NULL)
summaryRprof("profile.out")

profvis (interactive HTML profiler):

library(profvis)
profvis({
    x <- runif(1e6)
    y <- sapply(x, function(v) v^2)    # bad — slow loop
    z <- x^2                            # good — vectorized
})

Shows flame graph + line-by-line time + memory.

microbenchmark for comparing alternatives:

library(microbenchmark)

x <- runif(10000)
microbenchmark(
    sapply_method = sapply(x, function(v) v^2),
    vector_method = x^2,
    times = 100
)

Reports min/mean/median/max for each. Vector ops typically 100-1000x faster.

Performance principles:

1. Vectorize. Already discussed — replaces loops with C-level ops.

2. Avoid copying. R uses copy-on-modify. Each result[i] <- v can trigger a full copy:

# slow — may copy result on each iteration
result <- c()
for (i in 1:n) result <- c(result, i^2)

# fast — pre-allocate
result <- numeric(n)
for (i in 1:n) result[i] <- i^2

Better: vectorize entirely.

3. Use efficient data structures.

  • data.table — much faster than data.frame for >100k rows
  • Matrix package for sparse matrices
  • bigstatsr / bigmemory for out-of-RAM datasets

4. Compile hot loops with Rcpp.

library(Rcpp)
cppFunction('
int sum_squares(int n) {
    int s = 0;
    for (int i = 1; i <= n; i++) s += i * i;
    return s;
}
')
sum_squares(1000)

Mixes C++ with R. Often 10-100x faster than equivalent R loop.

5. Parallelize.

library(parallel)

# Use cores
results <- mclapply(1:1000, slow_fn, mc.cores = 4)

# Or with future:
library(future.apply)
plan(multisession, workers = 4)
results <- future_lapply(1:1000, slow_fn)

6. Replace S4/refclass dispatch in hot loops. Method dispatch isn't free.

7. Use which.max, which.min, pmax, pmin instead of writing loops:

# slow
best <- 1
for (i in 2:length(x)) if (x[i] > x[best]) best <- i

# fast
best <- which.max(x)

Memory profiling:

library(pryr)
object_size(x)               # actual size in bytes
mem_used()                    # total R memory

# Track allocations during a block:
library(profmem)
p <- profmem({
    big <- numeric(1e7)
    big2 <- big * 2
})
print(p)

bench package — modern microbenchmark:

library(bench)
bench::mark(
    sapply = sapply(1:1000, function(x) x^2),
    vector = (1:1000)^2,
    iterations = 100
)

Reports memory allocations too — often the bottleneck, not CPU.

Common pitfalls:

  • Growing vectors in loops with c()
  • Calling data.frame[i, j] in tight loops (slow indexing)
  • apply on data frames (converts to matrix internally — slow if mixed types)
  • Using rownames(df) (slow lookup)

Real-world: profile, find the bottleneck (it's almost never where you think), apply the relevant fix from above.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…