R的适用范围比句法糖更重要吗?


152

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

如果不正确,请使用代码段进行证明。请注意,通过矢量化进行的加速不计算在内。增速必须来自applytapplysapply,...)本身。

Answers:


152

applyR中的函数无法提供比其他循环函数(例如for)更高的性能。这样做的一个例外是lapply,它可能更快一些,因为它在C代码中比在R语言中执行更多的工作(有关此示例,请参见此问题)。

但总的来说,规则是您应该使用apply函数来提高清晰度,而不是为了提高性能

我还要补充一点,即应用函数没有副作用,这在使用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包名相同,即使包在本质上也等效)。这是中的sapply函数的简单示例snow

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


30
用于R的大多数多核程序包也通过apply功能族实现并行化。因此,结构化程序可以使用它们,从而可以以很小的边际成本将它们并行化。
沙皮犬

Sharpie-谢谢你!任何显示示例的想法(在Windows XP上)?
Tal Galili 2010年

5
我建议看一下snowfall包装,然后尝试在其插图中使用示例。 snowfall构建在snow包的顶部,并抽象化了并行化的细节,甚至进一步使得执行并行化apply功能变得非常简单。
Sharpie

1
@Sharpie,但请注意,foreach此后已经可用,因此在SO上似乎有很多询问。
阿里·弗里德曼

1
@Shane,在您回答的最顶部,您链接到另一个问题,例如lapplyfor循环“快一点” 的情况的示例。但是,在那里,我没有看到任何暗示。您只提到它lapply比快sapply,这是众所周知的事实(由于其他原因(sapply试图简化输出,因此必须进行大量数据大小检查和潜在转换))。与无关for。我想念什么吗?
flodel '16

70

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

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


啊,真是太好了:-)我实际上在想,是否有人会碰到我比较晚的答案。
Joris Meys 2010年

1
我总是按“活跃”排序。:)不知道如何概括您的答案;有时*apply会更快。但是我认为更重要的一点是副作用(用示例更新了我的答案)。
Shane 2010年

1
我认为,当您想在不同子集上应用功能时,套用特别快。如果有一个适用于嵌套循环的智能应用解决方案,我想应用解决方案也会更快。我猜在大多数情况下,应用速度不会很快,但是我绝对同意副作用。
乔里斯·梅斯

2
这有点不合时宜,但是对于此特定示例,data.table它甚至更快,并且我认为“更轻松”。library(data.table) dt<-data.table(X,Y,Z,key=c("Y,Z")) system.time(dt[,list(X_mean=mean(X)),by=c("Y,Z")])
dnlbrky

12
这种比较是荒谬的。tapply是用于特定任务的专用功能,这就是为什么它比for循环更快原因。它不能做for循环可以做的事情(而常规apply可以)。您正在将苹果与橙子进行比较。
埃迪(Eddi)'16

47

...正如我刚刚在其他地方写的,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

原始发现不再是真实的。forWindows 10 2核计算机上的循环速度更快。我使用5e6元素进行了此操作-循环为2.9秒,而的为3.1秒vapply
科尔

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次的平均值,因此将其移出系统即可。几乎没有影响)

不过,还有一点要注意的是,使用应用族功能的另一个原因独立于其性能,清晰度或没有副作用。一个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版本紧凑得多。


4
但是巴蒂尔的例子是实际一些,大部分时间在函数通常花费,而不是在循环。
hadley 2011年

9
为自己说话...:)...从某种意义上说,Shane的观点也许是现实的,但从同样的意义上说,分析是完全没有用的。人们在必须进行大量迭代时会关心迭代机制的速度,否则他们的问题仍然存在。任何功能都是如此。如果我写了一个花费0.001s的罪,而别人写了一个花费0.002的罪,谁在乎呢?好吧,一旦您必须做一堆它们,您就会在乎。
约翰

2
在12核3Ghz intel Xeon(64位)上,我得到的数字完全不同-for循环显着改善:对于您的三个测试,我得到2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528,并且vapply更好:1.19 0.00 1.19
naught101

2
确实会因OS和R版本而有所不同……从绝对意义上讲,CPU也是如此。我只是跑2.15.2在Mac上,并获得sapply50%的速度慢于forlapply快一倍。
约翰

1
在您的示例中,您的意思是设置y1:1e6,而不是numeric(1e6)(零的向量)。试图分配foo(0)z[0]遍地不说明还有一个典型的for循环使用。否则,该消息会出现。
flodel '16

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

但是,针对这些情况,我们已经有了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

7
请务必注意,(对于一小段代码)microbenchmark它比精确得多system.time。如果尝试比较system.time(f3(mat))system.time(f4(mat))几乎每次都会得到不同的结果。有时,只有适当的基准测试才能显示最快的功能。
米歇尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.