lapply vs for 循环 - 性能 R

Posted

技术标签:

【中文标题】lapply vs for 循环 - 性能 R【英文标题】:lapply vs for loop - Performance R 【发布时间】:2017-07-12 15:07: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循环的函数

第三个是C++中同样的函数使用Rcpp

这里根据 Roland 的功能剖析。 我不确定我能否以正确的方式解释它。 在我看来,大部分时间都花在子集上 Function profiling

【问题讨论】:

请准确。我在您的函数 f 中没有看到对 apply 的任何调用。 我建议你学习如何分析函数:adv-r.had.co.nz/Profiling.html 您的代码中有几个错误;首先,irissubdf[, 4] &lt;- 1 应该是irissubdf$y &lt;- 1,所以以后可以使用该名称,其次,weightf 中使用之前没有定义。我也不清楚&lt;&lt;- 在你的lapplyreplicate 命令中做正确的事情,但我不清楚它应该做什么。这也可能是两者之间的主要区别; &lt;&lt;- 必须处理环境,而另一个则不需要,虽然我不知道这可能会产生什么影响,但它不再是苹果与苹果之间的比较了。 感谢指出,我只是忘记复制初始化权重(和权重差异)的代码。我使用了 嗨,我出于好奇尝试删除 【参考方案1】:

首先,for 循环比lapply 慢是一个早已被揭穿的神话。 R 中的 for 循环已经提高了很多性能,目前至少与 lapply 一样快。

也就是说,您必须在此处重新考虑您对lapply 的使用。您的实现需要分配给全局环境,因为您的代码需要您在循环期间更新权重。这是不考虑lapply的正当理由。

lapply 是一个你应该使用它的副作用(或没有副作用)的函数。与for 循环相反,lapply 函数会自动将结果组合到一个列表中,并且不会影响您工作的环境。 replicate 也是如此。另请参阅此问题:

Is R's apply family more than syntactic sugar?

lapply 解决方案慢得多的原因是您使用它的方式会产生更多开销。

replicate 在内部只不过是 sapply,因此您实际上结合了 sapplylapply 来实现双循环。 sapply 会产生额外的开销,因为它必须测试结果是否可以简化。所以for 循环实际上会比使用replicate 更快。 在您的lapply 匿名函数中,您必须为每次观察访问x 和y 的数据框。这意味着 - 与你的 for 循环相反 - 例如函数 $ 每次都必须被调用。 因为您使用这些高端函数,所以您的“lapply”解决方案调用了 49 个函数,而您的 for 解决方案只调用了 26 个函数。lapply 解决方案的这些额外函数包括对 match 等函数的调用, structure, [[, names, %in%, sys.call, duplicated, ... 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),]

【讨论】:

我对此答案中声称的揭穿的任何参考资料都非常感兴趣。你能在这里提供一些吗?【参考方案2】:

还有更多关于何时使用forlapply 以及哪个“性能”更好的问题。有时速度很重要,有时内存很重要。更复杂的是,时间复杂度可能不是您所期望的 - 也就是说,可以在不同的范围内观察到不同的行为,从而使任何笼统的陈述无效,例如“比”或“至少和一样快”。最后,一个经常被忽视的性能指标是thought-to-code,未成熟的优化 yada yada。

也就是说,在Introduction to R 中,作者暗示了一些性能问题:

警告:R 代码中使用 for() 循环的频率远低于编译语言。采用“整个对象”视图的代码在 R 中可能会更清晰、更快速。

给定一个类似的用例输入输出,不考虑用户偏好,明显优于另一个?

基准 - 斐波那契数列

我将计算 1 的方法与 N Fibonacci numbers(受 benchmarkme 包的启发)进行比较,避开 2nd Circle 并确保每种方法的输入和输出相同。还包括四种额外的方法来火上浇油——矢量化方法和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,本身a debated topic。但是,当使用适当的*apply*,这里vapply,性能是相似的。但该选择可能会对内存使用产生很大影响,sapply 的内存效率明显低于vapply

深入了解这些方法的不同性能的原因。 for-loop 执行许多类型检查,导致一些开销。另一方面,lapply 遭受flawed 语言设计的困扰,其中延迟评估或使用承诺是有代价的,源代码确认XFUN 的参数.Internal(lapply) 是承诺.

矢量化方法很快,并且可能比forlapply 方法更可取。请注意,与其他方法相比,矢量化方法是如何不规则地增长的。但是,矢量化代码的美观性可能是一个问题:您更喜欢调试哪种方法?

总的来说,我想说lapplyfor 之间的选择不是普通R 用户应该考虑的。坚持最容易编写、思考和调试的东西,或者更不容易(沉默?)出错的东西。性能上的损失可能会被节省的写入时间抵消。对于性能关键的应用程序,请确保使用不同的输入大小运行一些测试并正确分块代码。

【讨论】:

【参考方案3】:

其实

我确实用最近解决的一个问题测试了差异。

自己试试吧。

在我的结论中,没有区别,但我的情况下 for 循环比 lapply 快得多。

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

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 你的输出是f1的两倍

以上是关于lapply vs for 循环 - 性能 R的主要内容,如果未能解决你的问题,请参考以下文章

使用lapply或for循环将多个csv文件拉入自己的R数据帧

对具有相同结构的几个数据集使用lapply并可能进行for循环以提取和计算每个数据帧的值

如何使 tabItem 仪表板中的 for 循环或 lapply 循环中的函数闪亮

犰狳 vs for 循环向量乘法

lapply为R中列表比较

避免使用for循环