所以,让我们说我想取向量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的情节:
log(N)vs log(t)的图
蓝色图是for循环方法,红色和绿色图是sapply方法。在常规图中,您可以看到,随着样本大小变大,for循环方法比sapply方法更受青睐,这根本不是我预期的。如果你看一下log-log图(为了更容易区分较小的N结果),我们看到sapply的预期结果比小N的循环更有效。
有人知道为什么sapply比样本大小的循环更慢?感谢。
答案 0 :(得分:4)
您没有考虑为结果向量Y1
分配空间所花费的时间。随着样本大小的增加,分配Y1
所需的时间占执行时间的更大份额,并且更换所需的时间变得更小。
sapply
总是为结果分配内存,这是因为样本大小增加时效率较低的一个原因。关于sapply
调用simplify2array
,gagolews也有一个非常好的观点。那(可能)增加了另一个副本。
经过一些测试后,看起来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)
之前已经提出了大部分要点,但是......
sapply()
使用lapply()
,然后使用simplify2array()
支付一次性格式化结果的费用。
lapply()
创建一个长向量,然后创建大量的短(长度为1)向量,而for循环生成一个长向量。
与for循环相比,写的sapply()
有一个额外的函数调用。
使用gcinfo(TRUE)
让我们看到垃圾收集器在运行,每种方法都会导致垃圾收集器运行多次 - 这可能非常昂贵,而且不是完全确定的。
点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更接近。