为什么sapply比样本大小的循环慢?

时间:2014-10-17 17:43:30

标签: r benchmarking

所以,让我们说我想取向量X = 2 * 1:N并将e提高到每个元素的指数。 (是的,我认识到这样做的最好方法是简单地通过向量化exp(X),但重点是将循环与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 vs t的情节: enter image description here

log(N)vs log(t)的图 enter image description here

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

有人知道为什么sapply比样本大小的循环更慢?感谢。

3 个答案:

答案 0 :(得分:4)

您没有考虑为结果向量Y1分配空间所花费的时间。随着样本大小的增加,分配Y1所需的时间占执行时间的更大份额,并且更换所需的时间变得更小。

sapply总是为结果分配内存,这是因为样本大小增加时效率较低的一个原因。关于sapply调用simplify2arraygagolews也有一个非常好的观点。那(可能)增加了另一个副本。


经过一些测试后,看起来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')

答案 1 :(得分:3)

之前已经提出了大部分要点,但是......

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

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

  3. 与for循环相比,写的sapply()有一个额外的函数调用。

  4. 使用gcinfo(TRUE)让我们看到垃圾收集器在运行,每种方法都会导致垃圾收集器运行多次 - 这可能非常昂贵,而且不是完全确定的。

    < / LI>

    点1 - 3需要在示例的人工上下文中解释 - exp()是一个快速函数,夸大了内存分配(2),函数评估(3)和一个函数的相对贡献 - 时间成本(1)。第4点强调需要以系统的方式复制时间。

    我首先加载了编译器和microbenchmark软件包。我专注于最大的尺寸

    library(compiler)
    library(microbenchmark)
    n <- 10^7
    

    在我的第一个实验中,我用简单赋值替换了exp(),并尝试了在for循环中表示结果的不同方法 - 数值的向量,或{{1}所暗示的数值向量列表}。

    lapply()

    因此编译for循环是值得的,生成一个向量列表需要相当大的成本。同样,这个内存成本被for循环体的简单性放大了。

    我的下一个实验探讨了不同的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

    *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()更稳健(我保证返回的类型)没有性能损失,因此它应该是我在此系列中的首选功能。

    最后,我将for迭代与lapply()进行了比较,结果是一个矢量列表。

    vapply()

    编译的for循环和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 是颈缩的;额外的函数调用几乎使vapply()的执行时间加倍(再次,这个效果被示例的简单性夸大了)。各种尺寸的相对速度似乎没有太大变化

答案 2 :(得分:0)

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

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

速度快得多:

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

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