R Performance Optimization

R is fast enough for most analyses by default. But when working with millions of rows, complex simulations, or production pipelines, slow code becomes a real problem. Performance optimization in R follows a clear path: measure first, identify the bottleneck, then fix it using the right tool.

The Optimization Mindset

Rule 1: Profile before optimizing
  → Do not guess where the slowdown is — measure it

Rule 2: Fix the algorithm first
  → A better algorithm beats faster code every time

Rule 3: Vectorize before everything else
  → R is built for vectors; loops fight the language

Rule 4: Use packages designed for speed
  → data.table, Rcpp, parallel — do not reinvent them

Measuring Performance

# system.time() — simple timing
system.time({
  x <- 1:1000000
  result <- sum(x^2)
})
#    user  system elapsed
#   0.021   0.001   0.022

# microbenchmark — accurate comparison of alternatives
library(microbenchmark)

microbenchmark(
  loop      = { r <- 0; for(i in 1:10000) r <- r + i },
  vectorized = sum(1:10000),
  times = 100
)
# Unit: microseconds
#        expr    min   mean    max
#        loop 3500.1 3612.4 4200.0
#  vectorized    4.2    4.8    8.1
# → vectorized is ~750x faster

Profiling with profvis

library(profvis)

profvis({
  data <- data.frame(x=rnorm(100000), y=rnorm(100000))
  model <- lm(y ~ x, data=data)
  pred  <- predict(model)
  hist(pred)
})
# Opens interactive flame graph — shows time spent in each function

Vectorization — The Biggest Win

n <- 1000000

# Slow: loop
slow_sum <- function(n) {
  total <- 0
  for (i in 1:n) total <- total + i
  total
}

# Fast: vectorized
fast_sum <- function(n) sum(1:n)

microbenchmark(slow_sum(n), fast_sum(n), times=10)
# slow: ~500ms    fast: ~2ms   → 250x faster

Pre-allocate Memory

# Slow: growing vector inside loop (copies memory each time)
slow_grow <- function(n) {
  result <- c()
  for (i in 1:n) result <- c(result, i^2)
  result
}

# Fast: pre-allocate then fill
fast_grow <- function(n) {
  result <- numeric(n)    # allocate once
  for (i in 1:n) result[i] <- i^2
  result
}

# Fastest: fully vectorized
fastest <- function(n) (1:n)^2

data.table — Fast Data Manipulation

library(data.table)

# Convert data frame to data.table
dt <- as.data.table(large_df)

# data.table syntax: dt[rows, columns, by]
dt[age > 25, .(avg_salary=mean(salary)), by=department]

# data.table is 5-50x faster than dplyr on large datasets (>1M rows)
# Uses in-place modification and binary search
Speed comparison (10M rows):
  Operation          dplyr     data.table   Speedup
  ─────────────────────────────────────────────────
  Group + summarise  2.8s      0.3s         9x
  Filter             1.2s      0.1s         12x
  Join               4.5s      0.5s         9x

Rcpp — C++ Speed for Critical Loops

library(Rcpp)

cppFunction('
double sum_cpp(NumericVector x) {
  double total = 0;
  for (int i = 0; i < x.size(); i++) {
    total += x[i];
  }
  return total;
}
')

x <- rnorm(1000000)
microbenchmark(
  r_sum   = sum(x),        # base R sum (already fast)
  cpp_sum = sum_cpp(x),    # C++ via Rcpp
  times = 100
)
# Both are very fast for simple sum — Rcpp shines for complex loops

Parallel Computing

library(parallel)

n_cores <- detectCores() - 1   # leave one core free
cat("Using", n_cores, "cores\n")

# mclapply — parallel version of lapply (Mac/Linux)
results <- mclapply(1:8, function(i) {
  Sys.sleep(1)    # simulate work
  i^2
}, mc.cores=n_cores)

# Windows: use parLapply instead
cl <- makeCluster(n_cores)
results <- parLapply(cl, 1:8, function(i) i^2)
stopCluster(cl)

Caching with memoise

library(memoise)

# Cache results of slow function
slow_function <- function(n) {
  Sys.sleep(1)    # simulate expensive computation
  sum(1:n)
}

fast_function <- memoise(slow_function)

system.time(fast_function(1000))   # 1.0 second (first call)
system.time(fast_function(1000))   # 0.0 seconds (cached!)

Memory Management

# Check memory usage
object.size(large_df)   # size of one object
pryr::mem_used()        # total R memory use

# Free memory
rm(large_df)
gc()                    # run garbage collector

# Use integer instead of numeric to save memory
x_num <- 1:1000000             # numeric: 8 MB
x_int <- 1L:1000000L           # integer: 4 MB
object.size(x_num)             # 8000040 bytes
object.size(x_int)             # 4000040 bytes

Quick Optimization Checklist

Priority    Action
──────────────────────────────────────────────────────────────────
1 (highest) Profile with profvis — find the actual bottleneck
2           Vectorize any loops that do the same thing to each element
3           Pre-allocate result containers before filling in a loop
4           Switch to data.table for large data frame operations
5           Cache repeated computations with memoise
6           Use parallel processing for independent tasks
7           Write C++ with Rcpp for unavoidable complex loops
8 (lowest)  Upgrade hardware — sometimes the simplest solution

Most R performance problems come from just two causes: growing data structures inside loops, and applying element-by-element logic where vectorized functions already exist. Fix these two issues in your code and you will solve 80% of performance problems without touching any advanced tools.

Leave a Comment

Your email address will not be published. Required fields are marked *