【问题标题】:Why does sapply scale slower than for loop with sample size?为什么 sapply 的缩放速度比具有样本大小的 for 循环慢?
【发布时间】:2014-10-17 17:43:30
【问题描述】:

假设我想取向量 X = 2*1:N 并将 e 提高到每个元素的指数。 (是的,我认识到最好的方法是简单地通过矢量化 exp(X),但这样做的重点是比较 for 循环和 sapply)。好吧,我通过渐进式尝试三种方法(一种使用 for 循环,两种使用 sapply 以不同方式应用)进行了测试,这些方法具有不同的样本大小并测量相应的时间。然后,我绘制每种方法的样本量 N 与时间 t 的关系。

每个方法都用“#####”表示。

k <- 20 
t1 <- rep(0,k) 
t2 <- rep(0,k)
t3 <- rep(0,k)
L <- round(10^seq(4,7,length=k))


for (i in 1:k) {
  X <- 2*1:L[i]
  Y1 <- rep(0,L[i])
  t <- system.time(for (j in 1:L[i]) Y1[j] <- exp(X[j]))[3] #####
  t1[i] <- t
}

for (i in 1:k) {
  X <- 2*1:L[i]
  t <- system.time( Y2 <- sapply(1:L[i], function(q) exp(X[q])) )[3] #####
  t2[i] <- t
}

for (i in 1:k) {
  X <- 2*1:L[i]
  t <- system.time( Y3 <- sapply(X, function(x) exp(x)) )[3] #####
  t3[i] <- t
}

plot(L, t3, type='l', col='green')
lines(L, t2,col='red')
lines(L, t1,col='blue')

plot(log(L), log(t1), type='l', col='blue')
lines(log(L), log(t2),col='red')
lines(log(L), log(t3), col='green')

我们得到以下结果。 N 与 t 的图:

log(N) 与 log(t) 的图

蓝色图是for循环方法,红色和绿色图是sapply方法。在常规图中,您可以看到,随着样本量变大,for 循环方法比 sapply 方法更受青睐,这根本不是我所期望的。如果您查看对数图(为了更容易区分较小的 N 结果),我们会看到 sapply 的预期结果比 for 循环更有效。

有人知道为什么 sapply 的缩放速度比带有样本大小的 for 循环慢吗?谢谢。

【问题讨论】:

  • @Bridgeburners sapply(...) == simplify2array(lapply(...))。 unlist(lapply(...)) 呢?只是为了完整 - 我很好奇
  • @Bridgeburners:顺便说一句,而不是plot(log(L), log(t1), type='l', col='blue') 试试plot(L, t1, log="xy")

标签: r benchmarking


【解决方案1】:

您没有考虑为结果向量Y1 分配空间所需的时间。随着样本量的增加,分配Y1 所花费的时间在执行时间中所占的份额越来越大,而进行替换所花费的时间所占的份额越来越小。

sapply 总是为结果分配内存,因此这是随着样本量的增加它会降低效率的原因之一。 gagolews 关于sapply 调用simplify2array 也有一个很好的观点。这(可能)增加了另一个副本。


经过更多测试,看起来lapply 仍然与包含 for 循环的字节编译函数大致相同或慢,因为对象变大了。除了do_lapply 中的这一行之外,我不知道该如何解释:

if (MAYBE_REFERENCED(tmp)) tmp = lazy_duplicate(tmp);

或者可能与lapply 构造函数调用的方式有关……但我主要是在猜测。

这是我用来测试的代码:

k <- 20 
t1 <- rep(0,k) 
t2 <- rep(0,k)
t3 <- rep(0,k)
L <- round(10^seq(4,7,length=k))
L <- round(10^seq(4,6,length=k))

# put the loop in a function
fun <- function(X, L) {
  Y1 <- rep(0,L)
  for (j in 1:L)
    Y1[j] <- exp(X[j])
  Y1
}
# for loops often benefit from compiling
library(compiler)
cfun <- cmpfun(fun)

for (i in 1:k) {
  X <- 2*1:L[i]
  t1[i] <- system.time( Y1 <- fun(X, L[i]) )[3]
}
for (i in 1:k) {
  X <- 2*1:L[i]
  t2[i] <- system.time( Y2 <- cfun(X, L[i]) )[3]
}
for (i in 1:k) {
  X <- 2*1:L[i]
  t3[i] <- system.time( Y3 <- lapply(X, exp) )[3]
}
identical(Y1, Y2)          # TRUE
identical(Y1, unlist(Y3))  # TRUE
plot(L, t1, type='l', col='blue', log="xy", ylim=range(t1,t2,t3))
lines(L, t2, col='red')
lines(L, t3, col='green')

【讨论】:

  • 当我运行代码“system.time(Y
  • @Bridgeburners:我并不是要暗示它会解释所有的差异,但这是一个的差异。额外的(匿名)函数调用也会增加一些开销。
【解决方案2】:

大部分观点之前已经提出过,但是...

  1. sapply() 使用lapply(),然后支付一次性使用simplify2array() 格式化结果的成本。

  2. lapply() 创建一个长向量,然后创建大量短(长度为 1)向量,而 for 循环生成一个长向量。

  3. 与 for 循环相比,所写的 sapply() 多了一个函数调用。

  4. 使用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() 的执行时间增加了一倍(同样,这种效果因示例的简单性而被夸大了)。在各种尺寸范围内,相对速度似乎没有太大变化

【讨论】:

    【解决方案3】:

    尝试删除每次迭代运行的多余函数 (x) 代码。它必须有很多开销。我没有将两者分开,但 for 循环还应包括所有相关工作,如苹果与苹果比较:

    t <- system.time(Y1 <- rep(0,L[i])) + system.time(for (j in 1:L[i]) Y1[j] <- exp(X[j]))[3] #####
    

    更快的 sapply:

    for (i in 1:k) {
      X <- 2*1:L[i]
      t <- system.time( Y4 <- sapply(X,exp )[3]) #####
      t4[i] <- t
    }
    

    它仍然较慢,但比前两个 sapply 更接近。

    【讨论】:

    • 我同意你的第一点,这是我应该做的。至于第二点……嗯,你是对的,使用已经定义的 exp 函数会更快。但矢量化也是如此。并不是我想要一个更快的解决方案,而是我想了解为什么这个解决方案更慢。我使用 apply 函数的大部分时间是当我需要将它与自制函数一起使用时,而不是 R 定义的函数(大多数时候倾向于允许向量化)。所以了解自制函数的“原因”对我来说会更有用。
    • 如果我在 sapply 之外定义函数然后在 apply 函数中使用它会更快吗?如 "f
    • 我仍然认为最大的区别在于一个额外的匿名函数。对于自制函数,将匿名函数保留在 sapply 中,然后在 for 循环中添加一个。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-09
    • 2019-03-26
    • 1970-01-01
    • 1970-01-01
    • 2019-12-08
    • 2014-07-21
    • 1970-01-01
    相关资源
    最近更新 更多