......关于执行时间和/或记忆。
如果不是这样,请使用代码段进行验证。请注意,矢量化的加速不计算在内。加速必须来自apply
(tapply
,sapply
,...)本身。
答案 0 :(得分:146)
R中的apply
函数未提供优于其他循环函数的性能(例如for
)。一个例外是lapply
,它可以快一点,因为它在C代码中比在R中做更多的工作(参见this question for an example of this)。
但总的来说,规则是 你应该使用apply函数来提高清晰度,而不是性能 。
我想补充一点, 应用函数有no side effects ,这对于使用R进行函数式编程是一个重要的区别。这可以被覆盖使用assign
或<<-
,但这可能非常危险。副作用也使程序更难理解,因为变量的状态取决于历史。
编辑:
只是用一个简单的例子来强调这一点,递归地计算Fibonacci序列;这可以多次运行以获得准确的度量,但重点是没有一种方法具有显着不同的性能:
> fibo <- function(n) {
+ if ( n < 2 ) n
+ else fibo(n-1) + fibo(n-2)
+ }
> system.time(for(i in 0:26) fibo(i))
user system elapsed
7.48 0.00 7.52
> system.time(sapply(0:26, fibo))
user system elapsed
7.50 0.00 7.54
> system.time(lapply(0:26, fibo))
user system elapsed
7.48 0.04 7.54
> library(plyr)
> system.time(ldply(0:26, fibo))
user system elapsed
7.52 0.00 7.58
编辑2:
关于R的并行包的使用(例如rpvm,rmpi,snow),这些通常提供apply
个族函数(即使foreach
包也基本相同,尽管有名称)。以下是sapply
中snow
函数的一个简单示例:
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
此示例使用套接字群集,不需要安装其他软件;否则你需要像PVM或MPI这样的东西(见Tierney's clustering page)。 snow
具有以下适用功能:
parLapply(cl, x, fun, ...)
parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parApply(cl, X, MARGIN, FUN, ...)
parRapply(cl, x, fun, ...)
parCapply(cl, x, fun, ...)
因为没有 side effects ,apply
函数应该用于并行执行。在for
循环中更改变量值时,它是全局设置的。另一方面,所有apply
函数可以安全地并行使用,因为更改是函数调用的本地函数(除非您尝试使用assign
或<<-
,在这种情况下您可以引入副作用)。毋庸置疑,关注局部变量和全局变量至关重要,尤其是在处理并行执行时。
编辑:
就副作用而言,这是一个简单的例子来证明for
和*apply
之间的区别:
> df <- 1:10
> # *apply example
> lapply(2:3, function(i) df <- df * i)
> df
[1] 1 2 3 4 5 6 7 8 9 10
> # for loop example
> for(i in 2:3) df <- df * i
> df
[1] 6 12 18 24 30 36 42 48 54 60
请注意父级环境中的df
如何被for
更改,而不是*apply
。
答案 1 :(得分:69)
有时加速可能很大,例如当您必须嵌套for循环以获得基于多个因子的分组的平均值时。在这里,您有两种方法可以得到完全相同的结果:
set.seed(1) #for reproducability of the results
# The data
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# the function forloop that averages X over every combination of Y and Z
forloop <- function(x,y,z){
# These ones are for optimization, so the functions
#levels() and length() don't have to be called more than once.
ylev <- levels(y)
zlev <- levels(z)
n <- length(ylev)
p <- length(zlev)
out <- matrix(NA,ncol=p,nrow=n)
for(i in 1:n){
for(j in 1:p){
out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
}
}
rownames(out) <- ylev
colnames(out) <- zlev
return(out)
}
# Used on the generated data
forloop(X,Y,Z)
# The same using tapply
tapply(X,list(Y,Z),mean)
两者都给出完全相同的结果,是具有平均值和命名行和列的5 x 10矩阵。但是:
> system.time(forloop(X,Y,Z))
user system elapsed
0.94 0.02 0.95
> system.time(tapply(X,list(Y,Z),mean))
user system elapsed
0.06 0.00 0.06
你去吧。我赢了什么? ; - )
答案 2 :(得分:45)
...正如我刚才在别处写的那样,vapply是你的朋友! ...它就像是sapply,但你也指定了返回值类型,这使得它更快。
> system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
user system elapsed
3.54 0.00 3.53
> system.time(z <- lapply(y, foo))
user system elapsed
2.89 0.00 2.91
> system.time(z <- vapply(y, foo, numeric(1)))
user system elapsed
1.35 0.00 1.36
答案 3 :(得分:27)
我在其他地方写过,像Shane这样的例子并没有真正强调各种循环语法之间的性能差异,因为时间都是在函数中花费而不是实际强调循环。此外,代码不公平地将for循环与没有内存的应用与应用返回值的族函数进行比较。这是一个稍微不同的例子,强调了这一点。
foo <- function(x) {
x <- x+1
}
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
# user system elapsed
# 4.967 0.049 7.293
system.time(z <- sapply(y, foo))
# user system elapsed
# 5.256 0.134 7.965
system.time(z <- lapply(y, foo))
# user system elapsed
# 2.179 0.126 3.301
如果您打算保存结果,那么应用族函数可以多多于语法糖。
(z的简单unlist只有0.2s所以lapply要快得多。在for循环中初始化z非常快,因为我给出了6次运行中最后5次的平均值,所以在系统之外移动.time几乎不会影响事情)
另外需要注意的是,使用应用族功能还有另一个原因,不论其性能,清晰度或副作用是否存在。 for
循环通常会在循环内尽可能地促进放置。这是因为每个循环都需要设置变量来存储信息(以及其他可能的操作)。应用语句往往偏向另一种方式。通常,您希望对数据执行多个操作,其中一些操作可以进行矢量化,但有些操作可能无法进行。在R中,与其他语言不同,最好将这些操作分开并运行在apply语句(或函数的矢量化版本)中未向量化的那些操作以及被矢量化为真向量操作的那些操作。这通常会极大地提高性能。
以Joris Meys为例,他用一个方便的R函数替换传统的for循环,我们可以用它来表示以更友好的方式编写代码的效率,以获得类似的加速而没有专门的函数。
set.seed(1) #for reproducability of the results
# The data - copied from Joris Meys answer
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# an R way to generate tapply functionality that is fast and
# shows more general principles about fast R coding
YZ <- interaction(Y, Z)
XS <- split(X, YZ)
m <- vapply(XS, mean, numeric(1))
m <- matrix(m, nrow = length(levels(Y)))
rownames(m) <- levels(Y)
colnames(m) <- levels(Z)
m
这比for
循环快得多,并且比内置的优化tapply
函数慢一点。这不是因为vapply
比for
快得多,而是因为它只在循环的每次迭代中执行一次操作。在这段代码中,其他一切都是矢量化的。在Joris Meys传统的for
循环中,每次迭代都会发生很多(7?)操作,并且只需要执行它就可以进行相当多的设置。另请注意,这比for
版本更紧凑。
答案 4 :(得分:3)
在向量的子集上应用函数时,tapply
可能比for循环快得多。例如:
df <- data.frame(id = rep(letters[1:10], 100000),
value = rnorm(1000000))
f1 <- function(x)
tapply(x$value, x$id, sum)
f2 <- function(x){
res <- 0
for(i in seq_along(l <- unique(x$id)))
res[i] <- sum(x$value[x$id == l[i]])
names(res) <- l
res
}
library(microbenchmark)
> microbenchmark(f1(df), f2(df), times=100)
Unit: milliseconds
expr min lq median uq max neval
f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656 100
f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273 100
然而, apply
,在大多数情况下并没有提供任何速度提升,在某些情况下可能会慢得多:
mat <- matrix(rnorm(1000000), nrow=1000)
f3 <- function(x)
apply(x, 2, sum)
f4 <- function(x){
res <- 0
for(i in 1:ncol(x))
res[i] <- sum(x[,i])
res
}
> microbenchmark(f3(mat), f4(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975 100
f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100 100
但是对于这些情况,我们有colSums
和rowSums
:
f5 <- function(x)
colSums(x)
> microbenchmark(f5(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909 100