for循环与apply函数族在性能上有何区别?

44

通常人们说应该优先选择lapply而不是for循环。但也有例外情况,比如Hadley Wickham在他的Advance R书中指出的一些情况。

(http://adv-r.had.co.nz/Functionals.html)(例如就地修改、递归等)。下面是其中之一。

仅为了学习目的,我尝试以函数形式重写感知器算法,以便进行相对性能基准测试。 来源(https://rpubs.com/FaiHas/197581)。

以下是代码。

# prepare input
data(iris)
irissubdf <- iris[1:100, c(1, 3, 5)]
names(irissubdf) <- c("sepal", "petal", "species")
head(irissubdf)
irissubdf$y <- 1
irissubdf[irissubdf[, 3] == "setosa", 4] <- -1
x <- irissubdf[, c(1, 2)]
y <- irissubdf[, 4]

# perceptron function with for
perceptron <- function(x, y, eta, niter) {

  # initialize weight vector
  weight <- rep(0, dim(x)[2] + 1)
  errors <- rep(0, niter)


  # loop over number of epochs niter
  for (jj in 1:niter) {

    # loop through training data set
    for (ii in 1:length(y)) {

      # Predict binary label using Heaviside activation
      # function
      z <- sum(weight[2:length(weight)] * as.numeric(x[ii, 
        ])) + weight[1]
      if (z < 0) {
        ypred <- -1
      } else {
        ypred <- 1
      }

      # Change weight - the formula doesn't do anything
      # if the predicted value is correct
      weightdiff <- eta * (y[ii] - ypred) * c(1, 
        as.numeric(x[ii, ]))
      weight <- weight + weightdiff

      # Update error function
      if ((y[ii] - ypred) != 0) {
        errors[jj] <- errors[jj] + 1
      }

    }
  }

  # weight to decide between the two species

  return(errors)
}

err <- perceptron(x, y, 1, 10)

### my rewriting in functional form auxiliary
### function
faux <- function(x, weight, y, eta) {
  err <- 0
  z <- sum(weight[2:length(weight)] * as.numeric(x)) + 
    weight[1]
  if (z < 0) {
    ypred <- -1
  } else {
    ypred <- 1
  }

  # Change weight - the formula doesn't do anything
  # if the predicted value is correct
  weightdiff <- eta * (y - ypred) * c(1, as.numeric(x))
  weight <<- weight + weightdiff

  # Update error function
  if ((y - ypred) != 0) {
    err <- 1
  }
  err
}

weight <- rep(0, 3)
weightdiff <- rep(0, 3)

f <- function() {
  t <- replicate(10, sum(unlist(lapply(seq_along(irissubdf$y), 
    function(i) {
      faux(irissubdf[i, 1:2], weight, irissubdf$y[i], 
        1)
    }))))
  weight <<- rep(0, 3)
  t
}

我没有期望由于前面提到的问题会有任何一致的改善。但是,当我使用lapplyreplicate时,我还是非常惊讶地看到了明显的恶化。

我使用microbenchmark库中的microbenchmark函数得出这些结果。

可能的原因是什么?可能是内存泄漏吗?

                                                      expr       min         lq       mean     median         uq
                                                        f() 48670.878 50600.7200 52767.6871 51746.2530 53541.2440
  perceptron(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10)  4184.131  4437.2990  4686.7506  4532.6655  4751.4795
 perceptronC(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10)    95.793   104.2045   123.7735   116.6065   140.5545
        max neval
 109715.673   100
   6513.684   100
    264.858   100

第一个函数是lapply/replicate函数。
第二个函数使用for循环。
第三个函数是使用RcppC ++中的相同函数。
根据Roland对该函数的分析,大部分时间似乎花在子集上。我不确定自己是否能正确解释它。请参见Function profiling

2
请精确点,我在您的函数f中没有看到任何对apply的调用。 - Roland
1
我建议你学习如何分析函数:http://adv-r.had.co.nz/Profiling.html - Roland
嗨,出于好奇我尝试删除<<-。当然,现在代码是错误的,但没有性能提升。因此,作用域分配不是原因。 - Federico Manigrasso
  1. 你的代码没有按照编写的样子运行;你可能想要 y 而不是 irissubdf$y
  2. 你的代码没有按照编写的样子工作;f() 的返回结果和 perceptron(*) 不一致。
- Hong Ooi
你是正确的,现在是正确的。 - Federico Manigrasso
显示剩余3条评论
3个回答

70
首先,已经被证明的一个迷思是for循环比lapply慢。在R中,for循环已经变得更加高效,目前至少和lapply一样快。
话虽如此,你必须重新考虑在这里使用lapply的方式。你的实现需要分配给全局环境,因为你的代码要求在循环期间更新权重。这是不考虑lapply的一个有效原因。 lapply是一个应该用于其副作用(或缺乏副作用)的函数。函数lapply会自动将结果组合成一个列表,并且不会干扰你所在的环境,与for循环相反。对于replicate也是同样的情况。参见这个问题: R的apply家族是否只是语法糖? 你的lapply解决方案之所以慢得多,是因为你使用它的方式创建了更多的开销。
  • replicate实际上就是在内部使用sapply,因此您实际上将sapplylapply结合起来实现了双重循环。由于sapply必须测试结果是否可以简化,因此它会创建额外的开销。因此,使用for循环比使用replicate更快。
  • 在您的lapply匿名函数中,您必须为每个观察值都访问x和y的数据框。这意味着与您的for循环相反,例如每次都必须调用$函数。
  • 由于您使用这些高端函数,因此“lapply”解决方案调用了49个函数,而您的for解决方案仅调用了26个。这些额外的lapply解决方案函数包括对matchstructure[[names%in%sys.callduplicated等函数的调用......所有这些函数都不需要您的for循环,因为该循环不执行任何这些检查。
如果你想知道这个额外开销是从哪里来的,可以查看replicateunlistsapplysimplify2array的内部代码。

你可以使用以下代码更好地了解在使用lapply时性能损失的位置。逐行运行此代码!

Rprof(interval = 0.0001)
f()
Rprof(NULL)
fprof <- summaryRprof()$by.self

Rprof(interval = 0.0001)
perceptron(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10) 
Rprof(NULL)
perprof <- summaryRprof()$by.self

fprof$Fun <- rownames(fprof)
perprof$Fun <- rownames(perprof)

Selftime <- merge(fprof, perprof,
                  all = TRUE,
                  by = 'Fun',
                  suffixes = c(".lapply",".for"))

sum(!is.na(Selftime$self.time.lapply))
sum(!is.na(Selftime$self.time.for))
Selftime[order(Selftime$self.time.lapply, decreasing = TRUE),
         c("Fun","self.time.lapply","self.time.for")]

Selftime[is.na(Selftime$self.time.for),]

6
我对这个回答所声称的反驳有很大兴趣,请问您能提供一些相关参考资料吗? - MadmanLee

13
有关何时使用forlapply以及哪个更"高效"的问题,还有其他一些要考虑的因素。有时候速度很重要,而其他时候内存很重要。更加复杂的是,时间复杂度可能不是你所期望的 - 也就是说,在不同的范围内可以观察到不同的行为,使任何概括性语句如"更快"或"至少与...一样快"变得无效。最后,一个经常被忽视的性能指标是思考到编码,过早的优化等等。
话虽如此,在R入门中,作者们暗示了一些性能方面的问题:

警告:在R代码中,使用for()循环的情况比编译语言少得多。在R中,采用“整个对象”视图的代码可能会更清晰、更快。

如果给定相似的用例输入输出,不考虑用户偏好,是否有一个明显优于另一个?

基准测试 - 斐波那契数列

我比较了计算1到N 斐波那契数(灵感来自benchmarkme包)的方法,避免使用第二圈,并确保每种方法的输入和输出都相同。还包括了四种额外的方法来增加一些难度 - 一种向量化方法和purrr::map,以及*apply变体vapplysapply

fib <- function(x, ...){
  x <- 1:x ; phi = 1.6180339887498949 ; v = \() vector("integer", length(x))
  bench::mark(
    vector = {
      y=v(); y = ((rep(phi, length(x))^x) - ((-rep(phi, length(x)))^-x)) / sqrt(5); y},
    lapply = {
      y=v(); y = unlist(lapply(x, \(.) (phi^. - (-phi)^(-.)) / sqrt(5)), use.names = F); y},
    loop = {
      y=v(); `for`(i, x, {y[i] = (phi^i - (-phi)^(-i)) / sqrt(5)}); y},
    sapply = {
      y=v(); y = sapply(x, \(.) (phi^. - (-phi)^(-.)) / sqrt(5)); y},
    vapply = {
      y=v(); y = vapply(x, \(.) (phi^. - (-phi)^(-.)) / sqrt(5), 1); y},
    map = {
      y=v(); y <- purrr::map_dbl(x, ~ (phi^. - (-phi)^(-.))/sqrt(5)); y
    }, ..., check = T
  )[c(1:9)]
}

这是一份按中位数时间排名的性能比较。
lapply(list(3e2, 3e3, 3e4, 3e5, 3e6, 3e7), fib) # n iterations specified separately
N = 300
  expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
1 vector       38.8us   40.9us    21812.    8.44KB     0     1000     0     45.8ms
2 vapply        500us    545us     1653.    3.61KB     1.65   999     1      604ms
3 sapply        518us    556us     1725.   12.48KB     0     1000     0      580ms
4 lapply      513.4us  612.8us     1620.       6KB     8.14   995     5    614.2ms
5 loop        549.9us  633.6us     1455.    3.61KB     8.78   994     6    683.3ms
6 map         649.6us  754.6us     1312.    3.61KB     9.25   993     7    756.9ms

N = 3000
1 vector      769.7us  781.5us     1257.    82.3KB     1.26   999     1   794.83ms
2 vapply       5.38ms   5.58ms      173.    35.2KB     0.697  996     4      5.74s
3 sapply       5.59ms   5.83ms      166.   114.3KB     0.666  996     4      6.01s
4 loop         5.38ms   5.91ms      167.    35.2KB     8.78   950    50      5.69s
5 lapply       5.24ms   6.49ms      156.    58.7KB     8.73   947    53      6.07s
6 map          6.11ms   6.63ms      148.    35.2KB     9.13   942    58      6.35s

N = 30 000
1 vector       10.7ms   10.9ms      90.9     821KB    0.918   297     3      3.27s
2 vapply       57.3ms   60.1ms      16.4  351.66KB    0.741   287    13      17.5s
3 loop         59.2ms   60.7ms      15.9     352KB    16.7    146   154      9.21s
4 sapply       59.6ms   62.1ms      15.7    1.05MB    0.713   287    13      18.2s
5 lapply       57.3ms   67.6ms      15.1     586KB    20.5    127   173      8.43s
6 map          66.7ms   69.1ms      14.4     352KB    21.6    120   180      8.35s

N = 300 000
1 vector        190ms    193ms      5.14    8.01MB    0.206   100     4     19.45s
2 loop          693ms    713ms      1.40    3.43MB    7.43    100   532      1.19m
3 map           766ms    790ms      1.26    3.43MB    7.53    100   598      1.32m
4 vapply        633ms    814ms      1.33    3.43MB    0.851   100    39      45.8s
5 lapply        685ms    966ms      1.06    5.72MB    9.13    100   864      1.58m
6 sapply        694ms    813ms      1.27   12.01MB    0.810   100    39      48.1s

N = 3 000 000
1 vector        3.17s    3.21s    0.312     80.1MB    0.249   20    16       1.07m
2 vapply        8.22s    8.37s    0.118     34.3MB    4.97    20    845      2.83m
3 loop           8.3s    8.42s    0.119     34.3MB    4.35    20    733      2.81m
4 map           9.09s    9.17s    0.109     34.3MB    4.91    20    903      3.07m
5 lapply       10.42s   11.09s    0.0901    57.2MB    4.10    20    909       3.7m
6 sapply       10.43s   11.28s    0.0862   112.1MB    3.58    20    830      3.87m

N = 30 000 000
1 vector        44.8s   45.94s   0.0214      801MB   0.00854  10      4       7.8m
2 vapply        1.56m     1.6m   0.0104      343MB   0.883    10    850        16m
3 loop          1.56m    1.62m   0.00977     343MB   0.366    10    374      17.1m
4 map           1.72m    1.74m   0.00959     343MB   1.23     10   1279      17.4m
5 lapply        2.15m    2.22m   0.00748     572MB   0.422    10    565      22.3m
6 sapply        2.05m    2.25m   0.00747    1.03GB   0.405    10    542      22.3m

# Intel i5-8300H CPU @ 2.30GHz / R version 4.1.1 / purrr 0.3.4

forlapply方法表现相似,但lapply在内存方面更为贪婪,在输入大小增加时略慢(对于此任务而言)。请注意,purrr::map的内存使用与for-loop相当,优于lapply本身一个有争议的话题。然而,当使用适当的*apply*,例如vapply时,性能类似。但选择可能对内存使用产生很大影响,sapplyvapply明显不够内存高效。

一瞥引擎盖下的内容揭示了这些方法性能不同的原因。for-loop执行许多类型检查,导致一些开销。另一方面,lapply遭受了一个有缺陷的语言设计,其中惰性评估或承诺使用会付出代价,源代码证实XFUN参数到.Internal(lapply)是承诺。
向量化方法很快,可能比forlapply方法更可取。请注意,与其他方法相比,向量化方法增长不规则。然而,向量化代码的美学可能是一个问题:你更喜欢哪种方法进行调试? 总的来说,我认为在和之间做出选择不是普通R用户应该考虑的事情。坚持使用最容易编写、思考和调试的方法或者那些较少(静默?)出错的方法。性能上的损失可能会被编写节省的时间所抵消。对于性能关键的应用程序,请确保使用不同的输入大小运行一些测试,并正确分块代码。

1

实际上,

我最近解决了一个问题,测试了两种方法的差异。

你可以自己尝试一下。

我的结论是,在我的情况下,使用for循环和lapply没有什么区别,但是for循环比lapply稍微快一些。

附注:我尽量保持使用相同的逻辑。

ds <- data.frame(matrix(rnorm(1000000), ncol = 8))  
n <- c('a','b','c','d','e','f','g','h')  
func <- function(ds, target_col, query_col, value){
  return (unique(as.vector(ds[ds[query_col] == value, target_col])))  
}  

f1 <- function(x, y){
  named_list <- list()
  for (i in y){
    named_list[[i]] <- func(x, 'a', 'b', i)
  }
  return (named_list)
}

f2 <- function(x, y){
  list2 <- lapply(setNames(nm = y), func, ds = x, target_col = "a", query_col = "b")
  return(list2)
}

benchmark(f1(ds2, n ))
benchmark(f2(ds2, n ))

正如您所看到的,我编写了一个简单的例程来基于数据框构建一个named_list。func函数提取列值,f1使用for循环迭代数据框,f2使用lapply函数。
在我的电脑上,我得到了这些结果:
test replications elapsed relative user.self sys.self user.child
1 f1(ds2, n)          100  110.24        1   110.112        0          0
  sys.child
1         0

&&

        test replications elapsed relative user.self sys.self user.child
1 f1(ds2, n)          100  110.24        1   110.112        0          0
  sys.child
1         0

你的脚本不是自给自足的。你可以指定 benchmark() 函数的 library(),并定义 ds2 吗? - coip
5
你的输出是“f1”的两倍。 - Dan Chaltiel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接