Programming for Applications

Chapter 24: Optimizing R Programs

Yu-You Liou (NTU)

Shih Chien University

2026-08-03

Making R Faster

Measure First, Then Optimize

The golden rule: never optimize blindly. Find where time and memory actually go, then fix that. R gives you tools for both.

Measuring Performance

Timing: system.time

The simplest measurement — wrap any expression:

system.time(for (i in 1:1e6) sqrt(i))
使用者   系統   流逝 
 0.008  0.000  0.009 

Three numbers: user time (work R itself did), system time (work the OS did on R’s behalf), elapsed time (wall clock). gcFirst=TRUE (the default) runs garbage collection first for a cleaner reading. (For careful microbenchmarks the modern microbenchmark and bench packages repeat and compare.)

Profiling: Rprof and summaryRprof

To see where the time goes across a whole program, profile it:

Rprof("Rprof.out", interval = 0.02, memory.profiling = FALSE)
do.stuff()                       # ... your code ...
Rprof(NULL)                      # stop profiling
summaryRprof("Rprof.out")        # which functions ate the time

Rprof samples the call stack every interval seconds and writes to a file; Rprof(NULL) stops it. summaryRprof ranks functions by self- and total time (and, with memory=, by allocation). This is how you find the one line consuming most of the runtime — often a surprise.

Monitoring Memory

gc()                             # garbage-collect AND report usage
          used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
Ncells  622036 33.3    1408715 75.3         NA  1408715 75.3
Vcells 1153062  8.8    8388608 64.0      24576  2012802 15.4
object.size("Hello world!")      # approximate size of one object
120 bytes
  • gc() forces garbage collection and reports memory in use.
  • object.size(x) estimates an object’s footprint — invaluable when a “small” data frame is secretly huge.
  • memory.profile() breaks usage down by object type. (The old Windows-only memory.size()/memory.limit() were retired in R 4.2.)
  • Rprofmem(filename, threshold=) logs large allocations (R must be built with memory profiling).

Optimizing Your Code

Use Vector Operations

R’s single biggest speed lever: replace iterative element-by-element code with whole-vector operations. The Chapter 8 example, dramatized — growing a vector in a loop versus working on it at once:

slow <- function(n) { v <- NA; for (i in 1:n) v[i] <- i^2; v }
fast <- function(n) { (1:n)^2 }
system.time(slow(50000))["elapsed"]
elapsed 
  0.005 
system.time(fast(50000))["elapsed"]
elapsed 
      0 

The vectorized version is dramatically faster and clearer. Two reasons: the loop’s body re-dispatches R functions tens of thousands of times, and growing a vector reallocates it on every step. Pre-allocating (v <- numeric(n)) helps; vectorizing helps far more.

Transform Problems to Use Built-in Functions

Many loops can be rephrased as calls to functions implemented in C:

  • the apply family — sapply, lapply, vapply, mapply, tapply (Chapters 9, 12);
  • matrix algebra — %*%, crossprod, rowSums, colMeans instead of nested loops;
  • outer, cumsum, diff, tabulate, findInterval for common patterns.
m <- matrix(1:1e6, ncol=1000)
system.time(apply(m, 2, sum))["elapsed"]      # readable
elapsed 
  0.003 
system.time(colSums(m))["elapsed"]            # built-in, far faster
elapsed 
      0 

The lesson: a built-in that does the loop inside C beats any R-level loop.

Lookup Performance: Environments as Hash Tables

Repeatedly looking values up by name? A named vector or list does a linear scan; an environment is a hash table with near-constant-time lookup. For thousands of keyed lookups, store the data in an environment:

e <- new.env()
assign("BG", 42, envir=e)        # or e[["BG"]] <- 42
get("BG", envir=e)               # O(1) lookup
[1] 42

The book benchmarks this against named-vector lookup and finds environments dramatically faster as the table grows — the trick behind many high-performance R packages.

The Byte-Code Compiler, and Beyond

R ships a byte-code compiler (compiler package) that speeds up pure-R functions — and since R 2.14, base and recommended packages are pre-compiled; since R 3.4, functions are JIT-compiled on use:

library(compiler)
slow.c <- cmpfun(slow)           # compile to byte code
system.time(slow.c(50000))["elapsed"]
elapsed 
  0.003 

Tip

The optimization ladder. (1) Vectorize. (2) Use built-in/C functions and matrix algebra. (3) Pre-allocate; use environments for lookups. (4) Compile hot functions. (5) Only then go parallel (Chapter 26) or rewrite the bottleneck in C++ (Rcpp). Always re-profile after each step — and stop when it’s fast enough.