Loops in R and Python: Who is faster?

This post is about R versus Python in terms of the time they require to loop and generate pseudo-random numbers. To accomplish the task, the following steps were performed in Python and R (1) loop 100k times (\(i\) is the loop index) (2) generate a random integer number out of the array of integers from 1 to the current loop index \(i\) (\(i\)+1 for Python) (3) output elapsed time at the probe loop steps: \(i\) (\(i\)+1 for Python) in [10, 100, 1000, 5000, 10000, 25000, 50000, 75000, 100000]

R code

library(magrittr)
#number of the loop iterations
n_elements <- 1e5
#probe points
x <- c(10,100,1000,5000,10000,25000,50000,75000,100000)
#for loop
t <- Sys.time()
vec <- NULL
elapsed <- NULL
for (i in seq_len(n_elements))
{
    vec <- c(vec, sample(i, size = 1, replace = T))
    if(i %in% x) 
        elapsed <- c(elapsed, as.numeric(difftime(Sys.time(), t, 'secs')))
}
#lapply function
t <- Sys.time()
vec <- NULL
elapsed_sapply <- lapply(seq_len(n_elements), function(i) {
    vec <- c(vec, sample(i, size = 1, replace = T))
    if(i %in% x) 
        return(as.numeric(difftime(Sys.time(), t, 'secs')))
}) %>% Filter(Negate(is.null), .) %>% unlist()

Python code

from numpy import random as rand
import datetime as dt

#number of the loop iterations
n_elements = int(1e5)
#probe points
x = [10,100,1000,5000,10000,25000,50000,75000,100000]

#for loop
t = dt.datetime.now()
vec = []
elapsed = []

for i in range(n_elements):
    vec.append(rand.choice(i+1, size=1, replace=True))
    if i+1 in x:
        elapsed.append((dt.datetime.now() - t).total_seconds())

Results

The result is presented on the plot below (click here to explore the distributions in Plotly).

Conclusions

The following conclusions can be drawn:

  • Python is faster than R, when the number of iterations is less than 1000. Below 100 steps, python is up to 8 times faster than R, while if the number of steps is higher than 1000, R beats Python when using lapply function!
  • Try to avoid using for loop in R, especially when the number of looping steps is higher than 1000. Use the function lapply instead.
  • Timing runaway of the R for loop starts at 10k looping steps.

If you have questions please comment below.

9 Comments

  1. NP
    Nikolay Pakulin December 29, 2020

    This “benchcmark” is not about loops. It is about “append a random number to a one dimensional array”
    For Python `numpy.random.choice` is a bad choice. It is very slow. `random.randint` is 20 times faster. On my system the loop with `choice` took 2.16 sec while the loop with `randint` finished in 0.11 sec.
    The `R` version with `sample` elapsed 10.16 seconds and with `runif` 17.65 seconds.

    BUT if we do want benchmarking pure loop-concat feature, then we shoudn’t use random numbers.
    In `R` let’s modify the assignment in the loop: `vec <- c(vec, i)`
    Benchmark: 10.02 seconds
    In Python: `vec.append(i)`
    Benchmark: 0.02 seconds

    Python's loop-append is five hundred times faster than R’s loop-concat.

    Reply
  2. OM
    Oliver Madsen October 9, 2019

    While this post is quite interesting, it doesn’t truely highlight how the computations should be performed or compare similar methods in R and python. For example: Note that vec is expanded while using the for loop, while the code example using lapply never expands (it stays null at each iteration).

    For a more accurate comparison of the loop and lapply function, one can run the code example:


    t <- Sys.time();
    vec <- NULL;
    elapsed_sapply <- lapply(seq_len(n_elements), function(i) {
    vec <% Filter(Negate(is.null), .) %>% unlist()

    Note the double arrow <<-, which will look for existing names in the parent.frame() of the function, which in this case is the globalenv().

    Additionally users with little experience in R, should be aware that expansion using val <- c(val, newval) is extremely inefficient in R, as R copies the vector at each expansion, before overwriting existing values, which in turn will cause extra calls to the garbage cleaner, and slower code. Efficient R code will pre-allocate the vector of outputs and overwrite existing values to avoid most of this overhead. This is in fact what `lapply` does. A better comparison between R and Python code (possibly still horrible) would thus be:


    t <- Sys.time()
    vec <- numeric(n_elements)
    elapsed <- numeric(length(x))
    j <- 1
    for (i in seq_len(n_elements))
    {
    vec[i] <- sample(i, size = 1, replace = T)
    if(i %in% x){
    elapsed[j] <- as.numeric(difftime(Sys.time(), t, units = 'secs'))
    j <- j + 1
    }
    }

    Which also has more comparable times to python code, while being slightly faster than equivalent code using lapply.. For completionist sake, an equivalent for loop could be

    elapsed_sapply <- numeric(length(x))
    j <- 1
    t <- Sys.time()
    vec <- lapply(seq_len(n_elements), function(i) {
    if(i %in% x){
    elapsed_sapply[j] <<- as.numeric(difftime(Sys.time(), t, units = 'secs'))
    j <<- j + 1
    }
    sample(i, size = 1, replace = T)
    })

    Note however even
    Comparison between lapply and for loops in R will vary from computer to computer, but will in general show that for loops are slightly faster than *apply functions.

    This in turn should make sense, as the *apply function regardless of code has to call some sort of loop, in order to execute the function. Calling a function, which in turn needs to handle more diverse calls, will thus require the function to have some overhead to handle different input correctly.

    Reply
  3. JH
    Joe Hoeller March 4, 2018

    Generally it’s a bad practice from a software engineering standpoint to loop through anything, unless you absolutely have no choice.

    Functional Recursion or tail recursion is best. Loops are generally prone to err, mutate and even block operations. If you can not do recursion, utilizing
    .map( ) or .reduce( ) is an excellent choice as well.

    The methods, Map and Reduce are available natively in Python, as well as in the Python Library called PyDash.

    Reply
    1. CT
      Cees Timmerman July 3, 2018

      Aren’t function calls even slower than for loops in CPython? Not to mention its lack of tail call optimization, making infinite recursive calls impossible.

      Reply
      1. JH
        Joe Hoeller July 3, 2018

        Python doesn’t enforce FP rules like Haskell will. I’m also not sure why you’d want infinite recursion; if you recourse thru a tree no matter it’s shape, you should do it until you’ve searched every “branch” & then stop.

        I also think the rule is avoid function calls in the body of a loop.

        When you avoid writing loops you maintain correctness, it’s more accurate. If speed is a concern then make immutable streams as monads in parallel.

        Also – optimization is pointless unless you need it. 95% of the time, you won’t.

        Reply
        1. CT
          Cees Timmerman July 5, 2018

          It’s part of the ECMAScript6 standard, and indeed many hip FP people like it. Still, even SICP had you rewrite such code to use loops, like so: http://glat.info/fext/

          I don’t know how to picture “immutable streams as monads in parallel”, but did happen to use reduce() in a little benchmark here: https://stackoverflow.com/questions/48031283/why-is-php7-so-much-faster-than-python3-in-executing-this-simple-loop/51158913#51158913

          Reply
          1. JH
            Joe Hoeller July 5, 2018

            I did some research, if performance is an issue, while loops still outperform map or reduce (FoldL/FoldR), however you can gain some inaccuracy. So it comes down to a decision by developer:

            Do I need performance?
            Do I need accuracy?
            Do I need both?

            Performance: while loop
            Accuracy: FoldL/FoldR
            Both: Make a while loop and enforce immutable constants on both value and type. If you can, introduce parametric polymorphism.

          2. CT
            Cees Timmerman July 5, 2018

            Anaconda’s CPython runs that little benchmark no faster than the official CPython.

            Side effects should be clearly indicated in the code and documentation, and Python has a nice balance of grammar and libraries. Lisp is too basic and Java too massive, imo.

      2. JH
        Joe Hoeller July 3, 2018

        I’ll also note that I used Anaconda 3.6, which has been optimized – it’s a high performance compute version of python, so I have yet to feel any performance issues.

        Reply

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.