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.
