大部分观点之前已经提出过,但是...
sapply() 使用lapply(),然后支付一次性使用simplify2array() 格式化结果的成本。
lapply() 创建一个长向量,然后创建大量短(长度为 1)向量,而 for 循环生成一个长向量。
与 for 循环相比,所写的 sapply() 多了一个函数调用。
使用gcinfo(TRUE) 可以让我们看到垃圾收集器的运行情况,每种方法都会导致垃圾收集器运行多次——这可能非常昂贵,而且不是完全确定的。
第 1 - 3 点需要在示例的人工上下文中进行解释——exp() 是一个快速函数,夸大了内存分配 (2)、函数评估 (3) 和一次性成本的相对贡献(1)。第 4 点强调需要以系统的方式复制时间安排。
我首先加载了编译器和微基准测试包。我只关注最大的尺寸
library(compiler)
library(microbenchmark)
n <- 10^7
在我的第一个实验中,我用简单的赋值替换了 exp(),并尝试了在 for 循环中表示结果的不同方式——数值向量,或 lapply() 所暗示的数值向量列表。
fun0n <- function(n) {
Y1 <- numeric(n)
for (j in seq_len(n)) Y1[j] <- 1
}
fun0nc <- compiler::cmpfun(fun0n)
fun0l <- function(n) {
Y1 <- vector("list", n)
for (j in seq_len(n)) Y1[[j]] <- 1
}
fun0lc <- compiler::cmpfun(fun0l)
microbenchmark(fun0n(n), fun0nc(n), fun0lc(n), times=5)
## Unit: seconds
## expr min lq mean median uq max neval
## fun0n(n) 5.620521 6.350068 6.487850 6.366029 6.933915 7.168717 5
## fun0nc(n) 1.852048 1.974962 2.028174 1.984000 2.035380 2.294481 5
## fun0lc(n) 1.644120 2.706605 2.743017 2.998258 3.178751 3.187349 5
因此编译 for 循环是值得的,而且生成向量列表的成本相当高。同样,这种内存成本被 for 循环体的简单性放大了。
我的下一个实验探索了不同的*apply()
fun2s <- function(n)
sapply(raw(n), function(i) 1)
fun2l <- function(n)
lapply(raw(n), function(i) 1)
fun2v <- function(n)
vapply(raw(n), function(i) 1, numeric(1))
microbenchmark(fun2s(n), fun2l(n), fun2v(n), times=5)
## Unit: seconds
## expr min lq mean median uq max neval
## fun2s(n) 4.847188 4.946076 5.625657 5.863453 6.130287 6.341282 5
## fun2l(n) 1.718875 1.912467 2.024325 2.141173 2.142004 2.207105 5
## fun2v(n) 1.722470 1.829779 1.847945 1.836187 1.845979 2.005312 5
sapply()中的简化步骤成本很大; vapply() 比 lapply() 更健壮(我保证返回的类型)没有性能损失,所以它应该是我在这个系列中的首选函数。
最后,我将 for 迭代与 vapply() 进行了比较,结果是向量列表。
fun1 <- function(n) {
Y1 <- vector("list", n)
for (j in seq_len(n)) Y1[[j]] <- exp(0)
}
fun1c <- compiler::cmpfun(fun1)
fun3 <- function(n)
vapply(numeric(n), exp, numeric(1))
fun3fun <- function(n)
vapply(numeric(n), function(i) exp(i), numeric(1))
microbenchmark(fun1c(n), fun3(n), fun3fun(n), times=5)
## Unit: seconds
## expr min lq mean median uq max neval
## fun1c(n) 2.265282 2.391373 2.610186 2.438147 2.450145 3.505986 5
## fun3(n) 2.303728 2.324519 2.646558 2.380424 2.384169 3.839950 5
## fun3fun(n) 4.782477 4.832025 5.165543 4.893481 4.973234 6.346498 5
microbenchmark(fun1c(10^3), fun1c(10^4), fun1c(10^5),
fun3(10^3), fun3(10^4), fun3(10^5),
times=50)
## Unit: microseconds
## expr min lq mean median uq max neval
## fun1c(10^3) 199 215 230 228 241 279 50
## fun1c(10^4) 1956 2016 2226 2296 2342 2693 50
## fun1c(10^5) 19565 20262 21671 20938 23410 24116 50
## fun3(10^3) 227 244 254 254 264 295 50
## fun3(10^4) 2165 2256 2359 2348 2444 2695 50
## fun3(10^5) 22069 22796 23503 23251 24393 25735 50
编译后的for循环和vapply()并驾齐驱;额外的函数调用几乎使vapply() 的执行时间增加了一倍(同样,这种效果因示例的简单性而被夸大了)。在各种尺寸范围内,相对速度似乎没有太大变化