R 的应用类是否比句法糖更多?

关于执行时间和/或内存。

如果这不是真的,那么用代码片段证明它。请注意,向量化的加速不计算在内。加速必须来自 apply(tapplysapply,...)本身。

22479 次浏览

与其他循环函数(例如 for)相比,R 中的 apply函数不能提供更好的性能。一个例外是 lapply,它可以稍微快一点,因为它在 C 代码中比在 R 中做更多的工作(参见 这个问题作为一个例子)。

但一般来说,规则是 你应该使用一个 application 函数来保证清晰度,而不是性能

我要补充的是 应用函数没有副作用,当涉及到使用 R 的函数式编程时,这是一个重要的区别。这可以通过使用 assign<<-来覆盖,但是这可能非常危险。副作用也使程序更难理解,因为变量的状态取决于历史。

编辑:

为了强调这一点,举一个简单的例子,递归计算斐波那契数列; 这可以运行多次以得到一个准确的度量,但关键是没有一个方法具有明显不同的性能:

> 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包本质上也是等价的)。下面是 snowsapply函数的一个简单例子:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)

此示例使用一个套接字集群,不需要安装其他软件; 否则将需要 PVM 或 MPI 之类的东西(请参见 Tierney 的聚类页面)。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, ...)

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改变的。

有时候加速可能会很大,比如当您必须嵌套 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

给你。我赢了什么? ; -)

我在其他地方写过,像 Shane 这样的例子并没有真正强调各种循环语法之间的性能差异,因为所有的时间都花在函数中,而不是实际强调循环。此外,代码不公平地将没有内存的 for 循环与返回值的 application 族函数进行比较。这里有一个稍微不同的例子,它强调了这一点。

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.2 s,所以 lapplication 要快得多。在 for 循环中初始化 z 非常快,因为我给出了6次运行中最后5次的平均值,所以在 system. time 外移动几乎不会影响到任何东西)

不过,还有一点需要注意的是,使用应用家族函数还有另一个原因,与它们的性能、清晰度或缺乏副作用无关。for循环通常促进尽可能多地放入循环中。这是因为每个循环都需要设置变量来存储信息(以及其他可能的操作)。应用语句倾向于偏向于相反的方向。通常需要对数据执行多个操作,其中有几个可以向量化,但有些可能无法向量化。在 R 语言中,与其他语言不同的是,最好将这些操作分离出来,运行那些在 application 语句(或函数的向量化版本)中没有向量化的操作和那些作为真向量操作向量化的操作。这通常会极大地提高性能。

以 Joris Meys 为例,他用一个方便的 R 函数替换了传统的 for 循环,我们可以使用它来展示以更加 R 友好的方式编写代码的效率,以便在没有专用函数的情况下实现类似的加速。

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功能慢一点点。这不是因为 vapplyfor快得多,而是因为它在循环的每次迭代中只执行一个操作。在这段代码中,其他所有内容都是向量化的。在乔里斯梅斯传统的 for循环许多(7?)每次迭代中都会发生一些操作,并且有相当多的设置只是为了让它执行。还要注意这比 for版本要紧凑得多。

就像我刚才在别处写的那样,维普尔是你的朋友! ... 就像 sapplication 一样,但是你也可以指定返回值类型,这样会快很多。

foo <- function(x) x+1
y <- numeric(1e6)


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

2020年1月1日更新:

system.time({z1 <- numeric(1e6); for(i in seq_along(y)) z1[i] <- foo(y[i])})
#   user  system elapsed
#   0.52    0.00    0.53
system.time(z <- lapply(y, foo))
#   user  system elapsed
#   0.72    0.00    0.72
system.time(z3 <- vapply(y, foo, numeric(1)))
#   user  system elapsed
#    0.7     0.0     0.7
identical(z1, z3)
# [1] TRUE

当在向量的子集上应用函数时,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

但对于这些情况,我们有 colSumsrowSums:

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