1

I've been toying with comparing vectorised R code with non-vectorised R code, and noticed that functions appear to use more memory the first time they are run. Here is a reproducible example:

library(bench)

squares <- function(x)
{
        y <- x
        for(i in seq_along(x))
        {
            y[i] <- x[i]*x[i]
        }
        return(y)
}

x <- 1:100
bm <- mark(x^2, squares(x))
bm

The first time this is run, squares(x) uses a lot more memory (mem_alloc column) than x^2:

> bm
# A tibble: 2 x 13
  expression    min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 x^2             0 558.1ns  1387977.    1.27KB        0 10000     0     7.21ms
2 squares(x) 12.4µs  14.2µs    64885.    4.15MB        0 10000     0   154.12ms
# … with 4 more variables: result <list>, memory <list>, time <list>, gc <list>

But if I run the code again, I get very different results:

> bm <- mark(x^2, squares(x))
> bm
# A tibble: 2 x 13
  expression    min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 x^2             0 490.1ns  1430864.      848B     0    10000     0     6.99ms
2 squares(x) 12.9µs  16.4µs    57321.      448B     5.73  9999     1   174.44ms
# … with 4 more variables: result <list>, memory <list>, time <list>, gc <list>

If I run the benchmark again, I get the same results as the second time.

If, when I first start R, I run the functions prior to the benchmark, I get the following:

> 1^2
[1] 1
> squares(1)
[1] 1
> bm <- mark(x^2, squares(x))
> bm
# A tibble: 2 x 13
  expression    min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 x^2             0 977.1ns   993503.    1.27KB        0 10000     0     10.1ms
2 squares(x) 12.8µs  14.5µs    63713.      448B        0 10000     0      157ms
# … with 4 more variables: result <list>, memory <list>, time <list>, gc <list>

Note that the memory usage for squares(x) is as low as in the second run, but not the usage for x^2. If instead I run x^2 prior to the first benchmark, the memory used for x^2 drops to 848B.

Is this because the memory used for R's just-in-time compilation is included in the memory profiling the first time the function is run? If so, why is x^2 affected - isn't the ^ operator already compiled to bytecode? Have I misunderstood what the memory profiling in R does? Or is something else going on here?

MånsT
  • 904
  • 5
  • 19
  • 1
    `^` is not compiled to bytecode. It is a primitive function (meaning it is compiled C code). Anyway, I suggest you turn off the JIT byte-code compilation and benchmark again. That should answer some of your question. – Roland Jun 10 '20 at 08:47
  • @Roland: Many thanks for the suggestion - turning off the JIT compilation was revealing! I've posted the results as an answer. – MånsT Jun 10 '20 at 09:46

1 Answers1

1

Following Roland's comment (thanks!), I tried turning off the JIT compilation. The results indicate that the additional memory usage the first time the function is run indeed is due to compilation:

library(compiler)
library(bench)
enableJIT(0) # Turn of JIT compilation

squares <- function(x)
{
        y <- x
        for(i in seq_along(x))
        {
            y[i] <- x[i]*x[i]
        }
        return(y)
}

With the following results:

> x <- 1:100
> bm <- mark(x^2, squares(x))
> bm
# A tibble: 2 x 13
  expression    min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 x^2             0 490.1ns  1205874.    1.27KB      0   10000     0     8.29ms
2 squares(x) 81.3µs  96.9µs    10428.      448B     62.7  4488    27   430.39ms
# … with 4 more variables: result <list>, memory <list>, time <list>, gc <list>
> 
> # A second run:
> bm <- mark(x^2, squares(x))
> bm
# A tibble: 2 x 13
  expression    min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 x^2             0 559.1ns  1311094.      848B      0   10000     0     7.63ms
2 squares(x) 79.3µs  87.7µs    10749.      448B     80.0  4571    34   425.25ms
# … with 4 more variables: result <list>, memory <list>, time <list>, gc <list>

(As expected, the non-compiled function is slower, although that wasn't the point of the experiment.)

In addition, explicit compilation using cmpfun also removes the excess memory usage in the first run:

library(compiler)
squares <- cmpfun(squares)

yields

> x <- 1:100
> bm <- mark(x^2, squares(x))
> bm
# A tibble: 2 x 13
  expression    min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 x^2           1ns   560ns  1163023.    1.27KB        0 10000     0      8.6ms
2 squares(x) 11.1µs  13.5µs    71576.      448B        0 10000     0    139.7ms
# … with 4 more variables: result <list>, memory <list>, time <list>, gc <list>

for the first run.

MånsT
  • 904
  • 5
  • 19