From 884eae89814b0cb28c2ec8e09318ed45cb19ddf3 Mon Sep 17 00:00:00 2001
From: Christopher Paciorek While some of the strategies covered here are specific to R, many are built on principles that can guide your coding in other languages. You should be able to work through this tutorial in any working R installation, including through RStudio. To work through it using R linked to a fast linear algebra package, you may want to use a virtual machine developed here at Berkeley, the Berkeley Common Environment (BCE). BCE is a virtual Linux machine - basically it is a Linux computer that you can run within your own computer, regardless of whether you are using Windows, Mac, or Linux. This provides a common environment so that things behave the same for all of us. However, note that BCE has not been updated in a while. You should be able to work through this tutorial in any working R installation, including through RStudio. However, in many cases the R you are using may not be linked to a fast linear algebra package. This tutorial assumes you have a working knowledge of R. To create this HTML document, simply compile the corresponding R Markdown file in R as follows (the following will work from within BCE after cloning the repository as above). To create this HTML document, simply compile the corresponding R Markdown file in R as follows. While some of the strategies covered here are specific to R, many are built on principles that can guide your coding in other languages. You should be able to work through this tutorial in any working R installation, including through RStudio. However, in many cases the R you are using may not be linked to a fast linear algebra package. You should be able to work through this tutorial in any working R installation, including through RStudio. However, in many cases the R you are using may not be linked to a fast linear algebra package. This tutorial assumes you have a working knowledge of R. In general, user time gives the CPU time spent by R and system time gives the CPU time spent by the kernel (the operating system) on behalf of R. Operations that fall under system time include opening files, doing input or output, starting other processes, etc. To time code that runs very quickly, you may want to use the microbenchmark
-package. Of course one would generally only care about such timing if a larger operation does the quick calculation very many times. Here's a comparison of different ways of accessing an element of a dataframe. To time code that runs very quickly, you should use the microbenchmark
+package. Of course one would generally only care about accurately timing quick calculations if a larger operation does the quick calculation very many times. Here's a comparison of different ways of accessing an element of a dataframe. The rbenchmark package provides a nice wrapper function, benchmark,
-that automates timings and comparisons. microbenchmark is also a good way to compare slower calculations, e.g., doing a crossproduct via crossproduct() compared to “manually”: An alternative that also automates timings and comparisons is the benchmark function from the rbenchmark package, but there's not really a reason to use it when microbenchmark is available. In general, it's a good idea to repeat (replicate) your timing, as there is some stochasticity in how fast your computer will run a piece of code at any given moment. The Rprof function will show you how much time is spent in
different functions, which can help you pinpoint bottlenecks in your
code. The output from Rprof can be hard to decipher, so you
-may want to use the proftools package functions, which make use of
-Rprof under the hood. Here's a function that works with a correlation matrix such as one
-might have for time series data. Basically, it creates a matrix of time lags (dd) and
-computes the correlation between the outcome for all pairs of times, based on the time
-lag between the pair of times. Then it computes the Cholesky factor of the correlation
-matrix so that it can generate a random time series in the last line. The question
-one might ask is which part(s) of the code take the most time.0) This Tutorial
0) This Tutorial
-git clone https://github.com/berkeley-scf/tutorial-efficient-R
From 75e99ebc1122b3d252d2b92b43bca174eaf31ee6 Mon Sep 17 00:00:00 2001
From: Christopher Paciorek Rscript -e "library(knitr); knit2html('efficient-R.Rmd')"
0) This Tutorial
3.1) Benchmarking
## user system elapsed
-## 0.232 0.016 0.246
+## 0.231 0.020 0.251
system.time(rowMeans(x))
## user system elapsed
-## 0.024 0.000 0.024
+## 0.025 0.000 0.024
library(microbenchmark)
df <- data.frame(vals = 1:3, labs = c('a','b','c'))
@@ -316,41 +316,45 @@ 3.1) Benchmarking
-## Unit: microseconds
-## expr min lq mean median uq max neval cld
-## df[2, 1] 12.532 13.2260 13.79943 13.6360 13.946 18.980 100 b
-## df$vals[2] 6.731 7.9160 8.71348 8.3565 8.641 52.525 100 a
-## df[2, "vals"] 12.670 13.5355 14.36814 13.7850 14.136 44.447 100 b
+## expr min lq mean median uq max neval cld
+## df[2, 1] 13.570 14.0245 14.55557 14.1945 14.4675 29.628 100 b
+## df$vals[2] 1.033 1.4335 1.85085 1.9300 2.0540 7.692 100 a
+## df[2, "vals"] 13.529 13.8460 14.96311 14.1025 14.3445 79.596 100 b
library(rbenchmark)
-# speed of one calculation
+
-library(microbenchmark)
n <- 1000
x <- matrix(rnorm(n^2), n)
-benchmark(crossprod(x), replications = 10,
- columns=c('test', 'elapsed', 'replications'))
+microbenchmark(
+ t(x) %*% x,
+ crossprod(x),
+ times = 10)
## test elapsed replications
-## 1 crossprod(x) 0.481 10
+
-## Unit: milliseconds
+## expr min lq mean median uq max neval
+## t(x) %*% x 38.5048 44.10347 52.56555 46.44601 57.27781 74.89820 10
+## crossprod(x) 19.0293 19.07104 23.59746 20.53333 28.25155 32.17322 10
+## cld
+## b
+## a
# comparing different approaches to a task
-benchmark(
- {mns <- rep(NA, n); for(i in 1:n) mns[i] <- mean(x[i , ])},
- rowMeans(x),
- replications = 10,
- columns=c('test', 'elapsed', 'replications'))
+
-library(rbenchmark)
+# speed of one calculation
+benchmark(t(x) %*% x,
+ crossprod(x),
+ replications = 10,
+ columns=c('test', 'elapsed', 'replications'))
## test
-## 1 {\n mns <- rep(NA, n)\n for (i in 1:n) mns[i] <- mean(x[i, ])\n}
-## 2 rowMeans(x)
-## elapsed replications
-## 1 0.196 10
-## 2 0.024 10
+## test elapsed replications
+## 2 crossprod(x) 0.238 10
+## 1 t(x) %*% x 0.564 10
3.2) Profiling
makeTS <- function(param, len){
- times <- seq(0, 1, length = len)
- dd <- rdist(times)
- C <- exp(-dd/param)
- U <- chol(C)
- white <- rnorm(len)
- return(crossprod(U, white))
-}
+will probably want to use the proftools package functions, which make use of
+Rprof under the hood.
Here's a function that does the linear algebra to find the least squares solution in a linear regression, assuming x
+is the matrix of predictors, including a column for the intercept.
lr_slow <- function(y, x) {
+ xtx <- t(x) %*% x
+ xty <- t(x) %*% y
+ inv <- solve(xtx) ## explicit matrix inverse is slow and generally a bad idea numerically
+ return(inv %*% xty)
}
+
+
+Let's run the function with profiling turned on.
+ +## generate random observations and random matrix of predictors
+y <- rnorm(5000)
+x <- matrix(rnorm(5000*1000), nrow = 5000)
-## using proftools instead:
library(proftools)
-pd <- profileExpr(makeTS(0.1, 3000))
-hotPaths(pd)
+
+pd1 <- profileExpr(lr_slow(y, x))
+hotPaths(pd1)
+
+
+## path total.pct self.pct
+## lr_slow 100.00 0.00
+## . %*% (<text>:2) 44.32 44.32
+## . t (<text>:3) 29.55 0.00
+## . . t.default 29.55 29.55
+## . solve (<text>:4) 22.73 0.00
+## . . solve.default 22.73 21.59
+## . . . diag 1.14 1.14
+## . t (<text>:2) 2.27 0.00
+## . . t.default 2.27 2.27
+## . %*% (<text>:3) 1.14 1.14
-Here's the result for the makeTS function:
+hotPaths(pd1, value = 'time')
+
- path total.pct self.pct
- makeTS 100.00 0.00
- . ?? (#File 1: :4) 56.06 56.06
- . chol (#File 1: :5) 30.30 0.00
- . . standardGeneric 30.30 0.00
- . . . chol 30.30 0.00
- . . . . chol.default 30.30 30.30
- . rdist (#File 1: :3) 12.12 0.00
- . . .Call 12.12 12.12
- . crossprod (#File 1: :7) 1.52 0.00
- . . crossprod 1.52 0.00
- . . . base::crossprod 1.52 1.52
+## path total.time self.time
+## lr_slow 1.76 0.00
+## . %*% (<text>:2) 0.78 0.78
+## . t (<text>:3) 0.52 0.00
+## . . t.default 0.52 0.52
+## . solve (<text>:4) 0.40 0.00
+## . . solve.default 0.40 0.38
+## . . . diag 0.02 0.02
+## . t (<text>:2) 0.04 0.00
+## . . t.default 0.04 0.04
+## . %*% (<text>:3) 0.02 0.02
-Note the nestedness of the results. For example, 12 percent of the time was spent in the call to rdist, of which essentially all of that was spent in .Call, which is a call out to C code.
+The first call to hotPaths shows the percentage of time spent in each call, while the second shows the actual time.
-In this case, the results are not fully helpful, as 56 percent of the time is spent in other computations within makeTS that are not shown individually (see the “??” line).
+Note the nestedness of the results. For example, essentially all the time spent in solve is actually spent in solve.default. In this case solve is just a generic method that immediately calls solve.default.
-As we increase the number of time points,
-the time taken up by the Cholesky would increase since that calculation
-is order of \(n^{3}\) while the others are order \(n^{2}\) (more in the Linear Algebra unit).
+We can see that a lot of time is spent in doing the two crossproducts. So let's try using crossprod to make those steps faster.
-In this case, since the Cholesky and the main calculations in rdist, as well as exp,
-are all done in compiled C or Fortran code, there is probably not much we can do
-to speed this up (apart from using an optimized BLAS, which is essential). But in other cases profiling may reveal the slow steps in a piece of code.
+lr_medium <- function(y, x) {
+ xtx <- crossprod(x)
+ xty <- crossprod(x, y)
+ inv <- solve(xtx) ## explicit matrix inverse is slow and generally a bad idea numerically
+ return(inv %*% xty)
+}
-Note that Rprof works by sampling - every little while (the interval argument) during a calculation it finds out what function R is in and saves that information to the file given as the argument to Rprof. So if you try to profile code that finishes really quickly, there's not enough opportunity for the sampling to represent the calculation accurately and you may get spurious results.
+pd2 <- profileExpr(lr_medium(y, x))
+hotPaths(pd2)
+
+
+## path total.pct self.pct
+## lr_medium 100.00 0.00
+## . crossprod (<text>:2) 50.94 50.94
+## . solve (<text>:4) 35.85 0.00
+## . . solve.default 35.85 33.96
+## . . . diag 1.89 1.89
+## . %*% (<text>:5) 9.43 9.43
+## . crossprod (<text>:3) 3.77 3.77
+
+
+hotPaths(pd2, value = 'time')
+
+
+## path total.time self.time
+## lr_medium 1.06 0.00
+## . crossprod (<text>:2) 0.54 0.54
+## . solve (<text>:4) 0.38 0.00
+## . . solve.default 0.38 0.36
+## . . . diag 0.02 0.02
+## . %*% (<text>:5) 0.10 0.10
+## . crossprod (<text>:3) 0.04 0.04
+
+
+First note that this version takes about half the time of the previous one. Second note that a fair amount of time is spent computing the explicit matrix inverse using solve. (There's not much we can do to speed up the crossprod operations.) It's well known that one should avoid computing the explicit inverse if one can avoid it. Here's a faster version that avoids it.
+
+lr_fast <- function(y, x) {
+ xtx <- crossprod(x)
+ xty <- crossprod(x, y)
+ U <- chol(xtx)
+ tmp <- backsolve(U, xty, transpose = TRUE)
+ return(backsolve(U, tmp))
+}
+
+pd3 <- profileExpr(lr_fast(y, x))
+hotPaths(pd3)
+
+
+## path total.pct self.pct
+## lr_fast 100.00 0.00
+## . crossprod (<text>:2) 78.79 78.79
+## . chol (<text>:4) 12.12 0.00
+## . . chol.default 12.12 12.12
+## . crossprod (<text>:3) 6.06 6.06
+## . backsolve (<text>:6) 3.03 3.03
+
+
+hotPaths(pd3, value = 'time')
+
+
+## path total.time self.time
+## lr_fast 0.66 0.00
+## . crossprod (<text>:2) 0.52 0.52
+## . chol (<text>:4) 0.08 0.00
+## . . chol.default 0.08 0.08
+## . crossprod (<text>:3) 0.04 0.04
+## . backsolve (<text>:6) 0.02 0.02
+
+
+We can see we get another speedup from that final version of the code. (But beware my earlier caution that if comparing times between implementations, we should have replication.)
You might also check out profvis for an alternative to displaying profiling information
generated by Rprof.
+Note that Rprof works by sampling - every little while (the interval argument) during a calculation it finds out what function R is in and saves that information to the file given as the argument to Rprof. So if you try to profile code that finishes really quickly, there's not enough opportunity for the sampling to represent the calculation accurately and you may get spurious results.
+
Warning: Rprof conflicts with threaded linear algebra,
so you may need to set OMP_NUM_THREADS to 1 to disable threaded
linear algebra if you profile code that involves linear algebra.
@@ -471,8 +545,8 @@ 4.1) Pre-allocate memory
## test elapsed replications
-## 1 fun1(z) 2.347 20
-## 2 fun2(z) 0.021 20
+## 1 fun1(z) 2.104 20
+## 2 fun2(z) 0.026 20
## 3 fun3(z) 0.006 20
@@ -497,8 +571,8 @@ For lists, we can do this
@@ -586,7 +660,7 @@## test elapsed replications
-## 1 out <- apply(x, 1, mean) 2.615 10
-## 2 out <- rowMeans(x) 0.220 10
+## 1 out <- apply(x, 1, mean) 1.880 10
+## 2 out <- rowMeans(x) 0.212 10
We can 'sweep' out a summary statistic, such as subtracting @@ -708,7 +782,7 @@
## user system elapsed
-## 0.124 0.040 0.162
+## 0.121 0.024 0.144
Here's a trick for doing the sweep based on vectorized calculations, remembering @@ -720,7 +794,7 @@
## user system elapsed
-## 0.276 0.048 0.324
+## 0.304 0.021 0.325
identical(out, out2)
@@ -753,7 +827,7 @@ Are apply, lapply, sapply, etc. faster than loops
## user system elapsed
-## 0.288 0.016 0.302
+## 0.085 0.000 0.084
system.time({
@@ -765,7 +839,7 @@ Are apply, lapply, sapply, etc. faster than loops
## user system elapsed
-## 0.300 0.016 0.312
+## 0.085 0.000 0.084
And here's an example where sapply is much faster, because the core function evaluation at each iteration is very fast:
@@ -787,8 +861,8 @@## test elapsed replications
-## 1 fun2(z) 2.302 10
-## 2 fun4(z) 0.032 10
+## 1 fun2(z) 2.258 10
+## 2 fun4(z) 0.034 10
You'll notice if you look at the R code for lapply (sapply just calls lapply) that it calls directly out to C code, so the for loop is executed in compiled code.
@@ -803,7 +877,7 @@## test elapsed replications
-## 1 apply(mat, 1, sum) 0.038 10
-## 2 mat %*% rep(1, ncol(mat)) 0.002 10
+## 1 apply(mat, 1, sum) 0.032 10
+## 2 mat %*% rep(1, ncol(mat)) 0.003 10
## 3 rowSums(mat) 0.006 10
@@ -943,83 +1017,68 @@ However, looking things up by name can be slow relative to looking up by index. -Here's a toy example where we have a vector or list with a million elements and +Here's a toy example where we have a vector or list with 1000 elements and the character names of the elements are just the character versions of the indices of the elements.
-n <- 1000000
+n <- 1000
x <- 1:n
xL <- as.list(x)
nms <- as.character(x)
names(x) <- nms
names(xL) <- nms
-benchmark(
- x[500000], # index lookup in vector
- x["500000"], # name lookup in vector
- xL[[500000]], # index lookup in list
- xL[["500000"]], # name lookup in list
- replications = 10, columns=c('test', 'elapsed', 'replications'))
+microbenchmark(
+ x[500], # index lookup in vector
+ x["500"], # name lookup in vector
+ xL[[500]], # index lookup in list
+ xL[["500"]]) # name lookup in list
-## test elapsed replications
-## 2 x["500000"] 0.062 10
-## 1 x[5e+05] 0.000 10
-## 4 xL[["500000"]] 0.058 10
-## 3 xL[[5e+05]] 0.000 10
+## Unit: nanoseconds
+## expr min lq mean median uq max neval cld
+## x[500] 475 523.5 587.95 552.5 605.5 1278 100 a
+## x["500"] 6202 6269.5 7407.16 6305.5 6395.0 105028 100 b
+## xL[[500]] 185 219.5 394.80 251.0 273.5 8283 100 a
+## xL[["500"]] 12043 12108.5 12432.33 12149.5 12213.0 20153 100 c
Lookup by name is slow because R needs to scan through the objects
one by one until it finds the one with the name it is looking for.
In contrast, to look up by index, R can just go directly to the position of interest.
+Side note: there is a lot of variability across the 100 replications shown above. This might have to do with cache effects (see next section).
+
In contrast, we can look up by name in an environment very quickly, because environments in R use hashing, which allows for fast lookup that does not require scanning through all of the names in the environment. In fact, this is how R itself looks for values when you specify variables in R code.
xEnv <- as.environment(xL) # convert from a named list
-xEnv$"500000"
+xEnv$"500"
-## [1] 500000
+## [1] 500
# I need quotes above because numeric; otherwise xEnv$nameOfObject is fine
-xEnv[["500000"]]
-
-
-## [1] 500000
+xEnv[["500"]]
-benchmark(
- x[500000],
- xL[[500000]],
- xEnv[["500000"]],
- xEnv$"500000",
- replications = 10000, columns=c('test', 'elapsed', 'replications'))
-
-
-## test elapsed replications
-## 1 x[5e+05] 0.026 10000
-## 3 xEnv[["500000"]] 0.030 10000
-## 4 xEnv$"500000" 0.031 10000
-## 2 xL[[5e+05]] 0.016 10000
+## [1] 500
microbenchmark(
- x[500000],
- xL[[500000]],
- xEnv[["500000"]],
- xEnv$"500000")
+ x[500],
+ xL[[500]],
+ xEnv[["500"]],
+ xEnv$"500")
## Unit: nanoseconds
-## expr min lq mean median uq max neval cld
-## x[5e+05] 286 334.5 391.49 360.5 408 1881 100 a
-## xL[[5e+05]] 126 157.5 295.53 183.5 196 8172 100 a
-## xEnv[["500000"]] 1195 1221.5 1478.58 1239.0 1264 23034 100 b
-## xEnv$"500000" 1176 1218.5 1360.84 1258.5 1305 9762 100 b
+## expr min lq mean median uq max neval cld
+## x[500] 466 520.5 687.27 540.5 563.0 13909 100 b
+## xL[[500]] 168 190.5 252.75 218.5 245.0 3511 100 a
+## xEnv[["500"]] 159 170.5 228.14 195.5 219.5 3054 100 a
+## xEnv$"500" 199 222.5 297.29 277.0 298.0 2763 100 a
-The first benchmark indicates that lookup in an environment is nearly as fast as lookup by index in a vector or list, though the microbenchmark suggests that lookup in the environment is somewhat slower.
-
Cache-aware programming
In addition to main memory (what we usually mean when we talk about RAM), computers also have memory caches, which are small amounts of fast memory that can be accessed very quickly by the processor. For example your computer might have L1, L2, and L3 caches, with L1 the smallest and fastest and L3 the largest and slowest. The idea is to try to have the data that is most used by the processor in the cache.
@@ -1040,7 +1099,7 @@ Cache-aware programming
## user system elapsed
-## 1.116 0.316 1.430
+## 0.856 0.224 1.081
A = t(A)
@@ -1048,7 +1107,7 @@ Cache-aware programming
## user system elapsed
-## 1.804 0.272 2.079
+## 1.751 0.296 2.047
nr = 800
@@ -1062,8 +1121,8 @@ Cache-aware programming
## test elapsed replications
-## 1 apply(A, 2, mean) 0.014 10
-## 2 apply(tA, 1, mean) 0.015 10
+## 1 apply(A, 2, mean) 0.012 10
+## 2 apply(tA, 1, mean) 0.012 10