R loops are slow: How to deal with that


You may have noticed that R loops are slow. You will find out why it is so and how to deal with that.


When you start learning R, no one will tell you that R loops are slow. At least, I was not taught about this and I have not seen an explicit statement about this in R textbooks. Later on, you begin using R beyond trivial tasks and you discover that R loops often becomes a bottleneck of your scripts. You would wonder why it is so and how to deal with that. This is exactly what happened to me.

Indeed, R for loops are inefficient, especially if you use them wrong. Searching for why R loops are slow discovers that many users are wondering about this question. Below, I summarize my experience and online discussions regarding this issue by providing some trivial code examples.

R is an interpreted language

This is what you need to keep in mind when you write in R or any other interpreted language. Make interpreted language easy for the end-user comes at the costs of processing such a code. There is a lot of extra computing steps to interpret the user-friendly code into computer code and execute it. That is why a compiled language is much faster as it doesn’t carry the extra baggage of interpreted language.

Does this mean you need to learn C or similar languages? Of course, not, though it won’t hurt to know some C :-). You just need to be aware of this fact and try to write your R code in a way that makes it efficient.

Below, I provide some examples that will help you understand when I am talking about. I also think these examples can be used as the best practices for R loops programming.

Keep R loops code minimal

Let’s have a look at the example when even some extra characters that do nothing impact the processing speed.

Create a matrix with random numbers:

set.seed(123)
m <- matrix(runif(1000000, max = 100), ncol = 100)

Calculate row means:

loopmean <- function(x){
  v <- c()
  for (i in c(1:dim(x)[1])){
    v[i] <- mean(x[i,])
  }
  return(v)
}

system.time(loopmeanD <- loopmean(m))
#  user  system elapsed 
# 0.039   0.003   0.044

The function here doesn’t matter. I just picked up mean as the most trivial example. We are interested in the amount of time it takes to process this loop.

If we reuse the same code but add some extra brackets in mean(), it will take substantially longer to process:

loopmeanBrackets <- function(x){
  v <- c()
  for (i in c(1:dim(x)[1])){
    v[i] <- mean(((((((((((((((((x[i,])))))))))))))))))
  }
  return(v)
}

system.time(loopmeanBracketsD <- loopmeanBrackets(m))
#  user  system elapsed
# 0.051   0.000   0.050

identical(loopmeanD, loopmeanBracketsD)
# [1] TRUE

We have changed nothing in terms of math. It is still the same calculation as before. However, R needs to go through each ( and ) every loop cycle and this slows down the code a lot.

So, next time you write your loop, make it as minimal as possible in terms of character numbers.

Process by columns

R naturally process the data by columns faster than by row. If you need to loop through the columns, transform your data and loop through the columns:

loopmeanColumn <- function(x){
  v <- c()
  for (i in (1:dim(x)[2])){
    v[i] <- mean(x[,i])
  }
  return(v)
}

tm <- t(m)
system.time(loopmeanColumnD <- loopmeanColumn(tm))
#  user  system elapsed 
# 0.037   0.000   0.036

identical(loopmeanD, loopmeanColumnD)
# [1] TRUE

Allocate memory

R also processes loops faster when you allocate the memory for the output object. In this case, R just needs to fill in the cells in a vector instead of extending the vector every loop cycle.

vertorloopmean <- function(x){
  v <- vector(length = dim(x)[1])
  for (i in c(1:dim(x)[1])){
    v[i] <- mean(x[i,])
  }
  return(v)
}

system.time(vertorloopmeanD <- vertorloopmean(m))
#  user  system elapsed 
# 0.031   0.000   0.032 

identical(loopmeanD, vertorloopmeanD)
# [1] TRUE

Use apply

When you search online why R loops are slow, you are likely to find the advice to use apply because it is faster. I also thought that apply is faster than for loops until I did the small research for this blog-post. In fact, apply also loops through the data and often it seems to be a little faster than for loops because its code tends to be shorter:

applymean <- function(x){
  v <- apply(m, 1, mean)
  return(v)
}

system.time(applymeanD <- apply(m, 1, mean))
#  user  system elapsed 
# 0.035   0.004   0.038 
identical(loopmeanD, applymeanD)
# [1] TRUE

Processing by columns is also fater for apply:

applymeanColumn <- function(x){
  v <- apply(tm, 2, mean)
  return(v)
}

system.time(applymeanColumnD <- applymeanColumn(m))
#  user  system elapsed 
# 0.036   0.000   0.036
identical(loopmeanD, applymeanColumnD)
# [1] TRUE

Please, see the benchmarking of all loops below to get more details on how apply compares to for loops. In this case, it actually was not faster than the for loop.

Compile your functions

You can improve the performance of your function by compiling it to byte code. This is especially beneficial when your function code is long.

library(compiler)

loopmeanCompiled <- cmpfun(loopmean)

system.time(loopmeanCompiledD <- loopmeanCompiled(m))
#  user  system elapsed 
# 0.035   0.000   0.035 
identical(loopmeanD, loopmeanCompiledD)
# [1] TRUE

Parallelize

R has several libraries that allow parallelizing the processing between the core of your processor.

I usually use doParallel library for that. It is not beneficial in this mean example, because it takes longer to split the processes between cores and collect the results that to run everything on one core. However, when each loop cycle is long enough, parallelizing helps a lot.

library(doParallel)

registerDoParallel(cores=12)

system.time(loopParallelD <- foreach(i=1:dim(m)[1], .combine=c) %dopar% mean(m[i,]))
#  user  system elapsed 
# 1.173   0.157   1.042 
identical(loopmeanD, loopParallelD)
# [1] TRUE

Use Built-in functions

Everything described above helps only marginally. You can get some performance improvments with these tricks but you will neber beat the built-in R functions that call C code directly without interpretation step. Just look at how much faster is the built-in function to calculate row means:

system.time(rowMeanD <- rowMeans(m))
#  user  system elapsed 
# 0.002   0.000   0.002 
identical(loopmeanD, rowMeanD)
# [1] TRUE

So, before you write your function, make sure there is no R library with such function already.

Write in C++

There is also an option to write your code in C++ and compile it with Rcpp to R code. This will also result in a considerably faster code. But of course, you need to know some C++ for that.

#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector cRowMeans(NumericMatrix x) {
 int nrows = x.nrow();   
 NumericVector v(nrows);
 for (int i = 0; i < nrows; i++){
   v[i] = mean(x.row(i));
 }
 return v;
}

system.time(cRowMeansD <- cRowMeans(m))
#  user  system elapsed 
# 0.004   0.000   0.004
identical(loopmeanD, cRowMeansD)
# [1] TRUE

Benchmarking R loops

Using system.time() several times with the same function will produce little different results. Although the system.time presented above is comparable it does not fully reflect the reality. As I have mentioned above, only after benchmarking all these functions, I discovered that apply was not as fast as I expected. Moreover, in this particular code, it was slower than a simple for loop.

library(microbenchmark)

mbm <- microbenchmark("LoopRowExtraBrackets" = loopmeanBrackets(m),
                      "ApplyRow" = applymean(m),
                      "ApplyColumn" = applymeanColumn(tm),
                      "LoopRowCompiled" = loopmeanCompiled(m),
                      "LoopRow" = loopmean(m),
                      "LoopColumn" = loopmeanColumn(tm),
                      "LoopRowToVector" = vertorloopmean(m),
                      "CLoop"= cRowMeans(m),
                      "Built-in_rowMeans" = rowMeans(m),
                      check = 'equal', times=1000)

mbm
# Unit: milliseconds
#                 expr       min        lq      mean    median        uq       max neval  cld
# LoopRowExtraBrackets 44.693969 49.214291 53.174126 51.161645 53.941782 93.298477   100   d
#             ApplyRow 38.594932 44.356220 50.847681 46.949665 51.282932 88.668781   100   d
#          ApplyColumn 38.211502 44.035419 51.626075 47.071399 52.140827 94.046727   100   d
#              LoopRow 35.798877 40.832460 43.676707 42.363606 44.313524 80.957104   100   c 
#      LoopRowCompiled 33.665563 40.451894 42.942701 42.355379 44.258304 73.966924   100   c 
#           LoopColumn 34.566808 39.875668 42.668796 41.743636 44.099745 76.010563   100   c 
#      LoopRowToVector 32.187435 37.927207 40.912034 39.813388 42.197814 74.008110   100   c 
#             RowLoopC  2.794117  3.721194  5.664805  4.260946  6.525059 49.985055   100   b  
#    Built-in_rowMeans  1.571780  1.668413  1.815267  1.687677  1.791447  3.601554   100   a

And the visualization of these results:

library(ggplot2)
autoplot(mbm)

benchmarks of R loops

Code

You can download the R code and test everything yourself.

Conclusion

We use R not because of its speed but rather because of its ease of use. The most efficient R code will never be faster than the alternative C code. But knowing the behavior of R I described above will help you to make your R loops the fastest within the limitation of R as an interpreted language.

Written on October 1, 2019