159

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

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

4

5 回答 5

158

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

但一般来说,规则是您应该使用 apply 函数来清晰,而不是性能

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

编辑:

只是用一个递归计算斐波那契数列的简单例子来强调这一点;这可以运行多次以获得准确的测量值,但关键是没有一种方法具有显着不同的性能:

> 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函数 in的一个简单示例snow

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

本例使用socket集群,无需安装额外软件;否则您将需要 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函数都可以安全地并行使用,因为更改是函数调用的本地更改(除非您尝试使用assignor <<-,在这种情况下您可能会引入副作用)。不用说,注意局部变量和全局变量是至关重要的,尤其是在处理并行执行时。

编辑:

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

于 2010-02-16T20:15:10.140 回答
73

有时加速可能是巨大的,例如当您必须嵌套 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 

你去吧。我赢了什么?;-)

于 2010-08-27T12:51:40.683 回答
48

...正如我刚刚在其他地方写的,vapply 是你的朋友!...这就像 sapply,但您还指定了返回值类型,这使它更快。

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
于 2011-03-23T22:00:07.463 回答
29

我在其他地方写过,像 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.2 秒,因此 lapply 快得多。在 for 循环中初始化 z 非常快,因为我给出了 6 次运行中最后 5 次的平均值,因此将其移到 system.time 之外几乎不影响事物)

不过要注意的另一件事是,使用 apply 系列函数还有另一个原因,与它们的性能、清晰度或缺乏副作用无关。循环通常for会促进尽可能多地在循环内放置。这是因为每个循环都需要设置变量来存储信息(以及其他可能的操作)。Apply 语句往往偏向于另一种方式。通常,您希望对数据执行多项操作,其中一些可以矢量化,但有些可能无法矢量化。在 R 中,与其他语言不同,最好将这些操作分开并运行那些在 apply 语句(或函数的向量化版本)中未向量化的操作,以及那些被向量化为真正向量操作的操作。这通常会极大地提高性能。

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

于 2011-02-02T14:53:11.083 回答
5

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

但是对于这些情况,我们有colSumsand 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
于 2013-04-10T17:53:03.843 回答