R是否适用于家庭而不是语法糖?

ste*_*fen 146 r apply

......关于执行时间和/或记忆.

如果不是这样,请使用代码段进行证明.请注意,矢量化的加速不计算在内.增速必须来自apply(tapply,sapply,...)本身.

Sha*_*ane 150

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

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

我要补充一点,应用函数没有副作用,这对于使用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 
Run Code Online (Sandbox Code Playgroud)

编辑2:

关于R的并行包的使用(例如rpvm,rmpi,snow),这些通常提供apply家庭功能(即使foreach名称也是基本等同的包).这是一个简单的sapply函数示例snow:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
Run Code Online (Sandbox Code Playgroud)

此示例使用套接字群集,不需要安装其他软件; 否则你需要像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, ...)
Run Code Online (Sandbox Code Playgroud)

有意义的是,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
Run Code Online (Sandbox Code Playgroud)

请注意df父环境中的内容是如何更改的,for但不是*apply.

  • R的大多数多核包也通过`apply`系列函数实现并行化.因此,构建程序以便它们使用apply允许它们以非常小的边际成本进行并行化. (30认同)
  • 我建议看一下'降雪'包裹并在他们的小插图中尝试这些例子.`snowfall`建立在`snow`包之上,并抽象出并行化的细节,甚至进一步简化了执行并行化`apply`功能. (5认同)

Jor*_*eys 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)
Run Code Online (Sandbox Code Playgroud)

两者都给出完全相同的结果,是具有平均值和命名行和列的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 
Run Code Online (Sandbox Code Playgroud)

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

  • 这种比较是荒谬的.`tapply`是特定任务的专用函数,****为什么它比for循环更快.它不能做for循环可以做什么(常规`apply`可以).你将苹果与橙子进行比较. (11认同)
  • 这有点偏离主题,但对于这个具体的例子,`data.table`甚至更快,我认为"更容易".`library(data.table)``dt <-data.table(X,Y,Z,key = c("Y,Z"))``system.time(dt [,list(X_mean = mean(X) ),通过= C( "Y,Z")])` (2认同)
  • @eddi,如果人们通常会在单个“for”循环中运行结果列表之前使用 split() ,我同意。但人们不这么认为。他们要么使用两个嵌套的 for 循环,要么使用“tapply”(或更现代的方法)(如果他们更熟悉 R)。因此,我在用户级别而不是内部检查性能。无论如何,我认为我很老的答案的最后一行“开玩笑”是显而易见的。 (2认同)

Tom*_*mmy 46

......正如我刚才在别处写的那样,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 
Run Code Online (Sandbox Code Playgroud)


Joh*_*ohn 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 
Run Code Online (Sandbox Code Playgroud)

如果您打算保存结果,那么应用族函数可能远远超过语法糖.

(z的简单unlist只有0.2s所以lapply要快得多.在for循环中初始化z非常快,因为我给出了6次运行中最后5次的平均值,所以在系统之外移动了.几乎不影响事情)

还有一点需要注意的是,使用应用族功能还有另一个原因,不论其性能,清晰度或副作用是否存在.一个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
Run Code Online (Sandbox Code Playgroud)

这比for环路快得多,比内置的优化tapply功能慢一点.这不是因为它vapplyfor在循环的每次迭代中只执行一次操作要快得多.在这段代码中,其他一切都是矢量化的.在Joris Meys的传统for循环中,每次迭代都会发生很多(7?)操作,并且只需要执行它就可以进行相当多的设置.另请注意,这比for版本更紧凑.

  • 为自己说话... :) ...也许Shane在某种意义上是现实的,但在同样的意义上,分析是完全没用的.当人们必须进行大量迭代时,人们会关心迭代机制的速度,否则他们的问题无论如何都会在其他地方出现.任何功能都是如此.如果我写了一个需要0.001s的罪并且其他人写了一个需要0.002的人?好吧,只要你必须做一堆他们你关心. (9认同)
  • 但Shane的例子是现实的,因为大部分时间__is__通常花在函数上,而不是在循环中. (4认同)
  • 在64位12核3Ghz英特尔至强处理器上,我得到的数字大不相同-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` (2认同)
  • 它确实随操作系统和R版本......以及绝对意义上的CPU而变化.我只是在Mac上以2.15.2运行并且比"for"和"lapply"慢了两倍,速度快了两倍. (2认同)

Mic*_*ele 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

但对于这些情况,我们有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
Run Code Online (Sandbox Code Playgroud)

  • 值得注意的是(对于小段代码)“microbenchmark”比“system.time”要精确得多。如果您尝试比较“system.time(f3(mat))”和“system.time(f4(mat))”,您几乎每次都会得到不同的结果。有时只有适当的基准测试才能显示最快的功能。 (8认同)