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
forloop in R, especially when the number of looping steps is higher than 1000. Use the functionlapplyinstead. - Timing runaway of the R
forloop starts at 10k looping steps.
If you have questions please comment below.

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.
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
vecis expanded while using thefor loop, while the code example usinglapplynever expands (it stays null at each iteration).For a more accurate comparison of the loop and
lapplyfunction, 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 theparent.frame()of the function, which in this case is theglobalenv().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 beelapsed_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
lapplyandfor loopsin R will vary from computer to computer, but will in general show thatfor loopsare slightly faster than*applyfunctions.This in turn should make sense, as the
*applyfunction 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.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.
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.
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.
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
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.
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.
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.