# Install packages if needed (uncomment to run)
# install.packages(c("parallel", "foreach", "doParallel", "tictoc"))
# Load the essential packages
library(parallel) # Base R parallel functions
library(foreach) # For parallel loops
library(doParallel) # Backend for foreach
library(tictoc) # For timing comparisons
Getting Started with Parallel Computing in R
What is Parallel Computing and Why Use It?
Have you ever run an R script that took hours to complete? Parallel computing can help! Instead of running calculations one after another (sequentially), parallel computing allows you to run multiple calculations at the same time by using all available CPU cores on your computer.
Benefits: - Speed: Run code significantly faster - Efficiency: Make better use of your computer’s resources - Scalability: Handle larger datasets and more complex models
Quick Setup: Required Packages
Let’s start by installing and loading the packages we’ll need:
How Many Cores Do You Have?
The first step is to check how many CPU cores are available on your computer:
# Detect the number of CPU cores
detectCores()
[1] 16
It’s usually good practice to leave one core free for your operating system, so we’ll typically use detectCores() - 1
for our parallel operations.
Your First Parallel Code: The Basics
Let’s create a simple function that takes some time to run, then compare how long it takes to run sequentially versus in parallel:
# A function that takes time to execute
<- function(x) {
slow_function Sys.sleep(0.5) # Simulate computation time (half a second)
return(x^2) # Return the square of x
}
# Create a list of numbers to process
<- 1:10 numbers
Method 1: Using parLapply (Works on All Systems)
This method works on all operating systems including Windows:
# Step 1: Create a cluster of workers
<- makeCluster(detectCores() - 1)
cl
# Step 2: Export any functions our workers need
clusterExport(cl, "slow_function")
# Run the sequential version and time it
tic("Sequential version")
<- lapply(numbers, slow_function)
result_sequential toc()
Sequential version: 5.11 sec elapsed
# Run the parallel version and time it
tic("Parallel version")
<- parLapply(cl, numbers, slow_function)
result_parallel toc()
Parallel version: 0.5 sec elapsed
# Step 3: Always stop the cluster when done!
stopCluster(cl)
# Verify both methods give the same results
all.equal(result_sequential, result_parallel)
[1] TRUE
Method 2: Using mclapply (Unix/Mac Only)
If you’re on Mac or Linux, you can use this simpler approach:
# For Mac/Linux users only
tic("Parallel mclapply (Mac/Linux only)")
<- mclapply(numbers, slow_function, mc.cores = detectCores() - 1)
result_parallel toc()
The foreach Package: A More Intuitive Approach
Many R users find the foreach
package easier to understand and use. It works like a loop but can run in parallel:
# Step 1: Create and register a parallel backend
<- makeCluster(detectCores() - 1)
cl registerDoParallel(cl)
# Run sequential foreach with %do%
tic("Sequential foreach")
<- foreach(i = 1:10) %do% {
result_sequential slow_function(i)
}toc()
Sequential foreach: 5.08 sec elapsed
# Run parallel foreach with %dopar%
tic("Parallel foreach")
<- foreach(i = 1:10) %dopar% {
result_parallel slow_function(i)
}toc()
Parallel foreach: 0.58 sec elapsed
# Always stop the cluster when done
stopCluster(cl)
# Verify results
all.equal(result_sequential, result_parallel)
[1] TRUE
Combining Results with foreach
One of the great features of foreach
is how easily you can combine results:
# Create and register a parallel backend
<- makeCluster(detectCores() - 1)
cl registerDoParallel(cl)
# Sum all results automatically with .combine='+'
tic("Parallel sum of squares")
<- foreach(i = 1:100, .combine = '+') %dopar% {
total ^2
i
}toc()
Parallel sum of squares: 0.11 sec elapsed
# Stop the cluster
stopCluster(cl)
# Verify the result
print(paste("Our result:", total))
[1] "Our result: 338350"
print(paste("Correct answer:", sum((1:100)^2)))
[1] "Correct answer: 338350"
A Real Example: Matrix Operations
Let’s try something more realistic. Matrix operations are perfect for parallelization:
# A more computationally intensive function
<- function(n) {
matrix_function # Create a random n×n matrix
<- matrix(rnorm(n*n), ncol = n)
m # Calculate eigenvalues (computationally expensive)
eigen(m)
return(sum(diag(m)))
}
# Let's process 8 matrices of size 300×300
<- rep(300, 8) matrix_sizes
Comparing Methods
Let’s compare how different methods perform:
# Sequential execution
tic("Sequential")
<- lapply(matrix_sizes, matrix_function)
sequential_result <- toc(quiet = TRUE)
sequential_time <- sequential_time$toc - sequential_time$tic
sequential_time
# Parallel with parLapply
<- makeCluster(detectCores() - 1)
cl clusterExport(cl, "matrix_function")
tic("parLapply")
<- parLapply(cl, matrix_sizes, matrix_function)
parlapply_result <- toc(quiet = TRUE)
parlapply_time <- parlapply_time$toc - parlapply_time$tic
parlapply_time stopCluster(cl)
# Parallel with foreach
<- makeCluster(detectCores() - 1)
cl registerDoParallel(cl)
tic("foreach")
<- foreach(s = matrix_sizes) %dopar% {
foreach_result matrix_function(s)
}<- toc(quiet = TRUE)
foreach_time <- foreach_time$toc - foreach_time$tic
foreach_time stopCluster(cl)
# Create a results table
<- data.frame(
results Method = c("Sequential", "parLapply", "foreach"),
Time = c(sequential_time, parlapply_time, foreach_time),
Speedup = c(1, sequential_time/parlapply_time, sequential_time/foreach_time)
)
# Display the results
results
Method Time Speedup
1 Sequential 1.69 1.000000
2 parLapply 0.25 6.760000
3 foreach 0.38 4.447368
Visualizing the Results
# Load ggplot2 for visualization
library(ggplot2)
# Plot execution times
ggplot(results, aes(x = reorder(Method, -Time), y = Time, fill = Method)) +
geom_bar(stat = "identity") +
labs(title = "Execution Time Comparison",
x = "Method", y = "Time (seconds)") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
# Plot speedup
ggplot(results, aes(x = reorder(Method, Speedup), y = Speedup, fill = Method)) +
geom_bar(stat = "identity") +
labs(title = "Speedup Comparison",
x = "Method", y = "Times faster than sequential") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
When to Use Parallel Computing
Parallel computing isn’t always the right choice. Here’s when to use it:
✅ Good for parallelization: - Independent calculations (like applying the same function to different data chunks) - Computationally intensive tasks (simulations, bootstrap resampling) - Tasks that take more than a few seconds to run sequentially
❌ Not good for parallelization: - Very quick operations (parallelization overhead may exceed the time saved) - Tasks with heavy dependencies between steps - I/O-bound operations (reading/writing files)
Quick Tips for Success
- Always stop your clusters with
stopCluster(cl)
when you’re done - Leave one core free for your operating system
- Start small and test with a subset of your data
- Watch your memory usage - each worker needs its own copy of the data
Next Steps
Once you’re comfortable with these basics, you can explore:
- The
future
package for more advanced parallel computing - The
furrr
package for parallel versions ofpurrr
functions - Parallel computing with large datasets using
data.table
ordplyr
- Distributed computing across multiple machines