【问题标题】:In R, why is sum so slow compared to others, such as cumsum?在 R 中,为什么 sum 与其他方法(例如 cumsum)相比如此缓慢?
【发布时间】:2014-05-08 00:52:17
【问题描述】:

我正在尝试实现一个需要非常快的函数,主要是因为它一遍又一遍地处理巨大的数据帧。

R 总是让我感到困惑,为什么它有时会有点慢,为什么有时会慢得离谱。 (不幸的是,它永远不会很快。)

无论如何,我一直认为,在可能的情况下,以某种方式将其推入 apply、sapply 或 lapply 时,事情会运行得更快,而不是放入循环中。我最近遇到了一个例子,它让我觉得背后还有更多的东西,如果我理解它,可能会对我未来的优化有很大帮助。

以下是我在相对强大的 Ubuntu Linux 机器上运行的一些计算:

system.time(sapply(1:1e5, sum))
user  system elapsed
35.130   0.000  35.128


system.time(sapply(1:1e5, cumsum))
user  system elapsed
0.110   0.000   0.108

是的,您正确阅读了这些数字:创建累积和数组的 cumsum 比仅提供简单总和快几个数量级。 (如果其他人可以在他们的机器上验证这些结果,那就太好了!)

我不明白这是怎么可能的,除非实现方式有很大不同。假设它们确实有很大差异,我想知道以什么方式,这样我就可以在寻找速度时寻找某些功能来避免。 (对于核心函数,我不知道如何查看它们的源代码。只输入函数名称而不带任何括号的标准方法不适用于核心函数。)

非常感谢!

【问题讨论】:

  • sapply(1:5, sum) 没有任何意义,因为它只会返回1:5。您是否只想总结这些数字?
  • 我同意这没有意义,因为你正在做sum(1) sum(2) 等等。另外,请检查一下:system.time(sapply(as.list(1:1e5), sum)) 比 w 快 way /o as.list。尽管如此,你的观点很有趣。为什么 sum(1) 比 cumsum(1) 慢?另外,我应该指出,我使用微基准测试了 sum(1) 和 cumsum(1),sum 是 553 纳秒,cumsum 是 433 纳秒。
  • cumsum 和朋友似乎没有na.rm 的能力。我想知道这是否起作用?
  • 您错误地假设“当以某种方式推入应用、sapply 或 lapply 而不是放入循环时,事情可以运行得更快”。那明显是错的。这些循环函数通常不会比for 循环快(前提是后者设置正确,即不会增长对象)。
  • 一个有趣的替代方案是sapply(1:1e5, function(x) sum(x)),它的返回或多或少类似于 cumsum。

标签: r performance


【解决方案1】:

遵循instructions for using operf 或多或少,我用单行sapply(1:1e5, sum) 创建了一个文件并运行

$ operf ~/bin/R-3-1-branch/bin/R -f sum.R
$ opreport -l ~/bin/R-3-1-branch/lib/libR.so |less

生产

CPU: Intel Sandy Bridge microarchitecture, speed 2.401e+06 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (No unit mask) count 100000
samples  %        image name               symbol name
835882   93.0929  libR.so                  RunGenCollect
27731     3.0884  libR.so                  SortNodes
9323      1.0383  libR.so                  AgeNodeAndChildren
2038      0.2270  libR.so                  CheckFinalizers
1593      0.1774  libR.so                  Rf_allocVector3
1222      0.1361  libR.so                  duplicate1
...

等等。大部分时间都花在垃圾收集器上(RunGenCollect -- 运行分代垃圾收集器)。于是我跑了

$ R -d gdb R
(gdb) run
> sapply(1:1e5, sum)
^C
(gdb) break RunGenCollect
(gdb) continue
Continuing.

Breakpoint 1, RunGenCollect (size_needed=50000) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:1504
1504        bad_sexp_type_seen = 0;
(gdb) where

生产的

#0  RunGenCollect (size_needed=50000) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:1504
#1  0x00007ffff789d354 in R_gc_internal (size_needed=50000) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:2825
#2  0x00007ffff789e99b in Rf_allocVector3 (type=13, length=100000, allocator=0x0) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:2563
#3  0x00007ffff788e1a5 in Rf_allocVector (type=13, length=100000) at /home/mtmorgan/src/R-3-1-branch/src/include/Rinlinedfuns.h:189
#4  0x00007ffff7831787 in duplicate1 (s=0x7ffff3b0b010, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:335
#5  0x00007ffff783371a in duplicate_child (s=0x7ffff3b0b010, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:199
#6  0x00007ffff783357a in duplicate_list (s=0x2c98b30, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:261
#7  0x00007ffff7830fc2 in duplicate1 (s=0x2c98b30, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:308
#8  0x00007ffff783371a in duplicate_child (s=0x2c98b30, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:199
#9  0x00007ffff783357a in duplicate_list (s=0x2c98a88, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:261
#10 0x00007ffff7830fc2 in duplicate1 (s=0x2c98a88, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:308
#11 0x00007ffff7830c7f in Rf_duplicate (s=0x2c98a88) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:132
#12 0x00007ffff79257f4 in do_summary (call=0x2c98a88, op=0x6259a0, args=0x303cf88, env=0x2c97f48) at /home/mtmorgan/src/R-3-1-branch/src/main/summary.c:462
...

这里的相关行是第462行

(gdb) up 12
#12 0x00007ffff79257f4 in do_summary (call=0x2c98a88, op=0x6259a0, args=0x303cf88, env=0x2c97f48) at /home/mtmorgan/src/R-3-1-branch/src/main/summary.c:462
462     PROTECT(call2 = duplicate(call));
(gdb) list
457     return ans;
458     }
459 
460     /* match to foo(..., na.rm=FALSE) */
461     PROTECT(args = fixup_NaRm(args));
462     PROTECT(call2 = duplicate(call));
463     SETCDR(call2, args);
464 
465     if (DispatchGroup("Summary", call2, op, args, env, &ans)) {
466     UNPROTECT(2);

通话正在重复

(gdb) call Rf_PrintValue(call)
FUN(1:100000[[5339L]], ...)

对于循环的每次迭代,触发垃圾回收。 not 为 cumsum 执行了类似的代码。长期以来一直如此,原因并非 100% 明显

$ svn annotate ~/src/R-3-1-branch/src/main/summary.c |less
...
 42643     ripley     /* match to foo(..., na.rm=FALSE) */
 42643     ripley     PROTECT(args = fixup_NaRm(args));
 42643     ripley     PROTECT(call2 = duplicate(call));
 42643     ripley     SETCDR(call2, args)
...
$ svn log -r42643
------------------------------------------------------------------------
r42643 | ripley | 2007-08-25 23:09:50 -0700 (Sat, 25 Aug 2007) | 1 line

make the rest of the group generics primitive
------------------------------------------------------------------------

R-devel 邮件列表中处理这件事会很有趣。并不是sum 特别慢,而是对垃圾收集器的调用开始支配执行时间。

嗯,仔细想想

sapply(1:1e5, function(x) sum(x))

cumsum 在同一个球场上运行。我认为这是因为原始版本中第 462 行的 duplicate 正在复制 1e5 元素,以准备选择要求和的第 i 个元素。相反,在function(x) sum(x) 中,向量已经是子集,所以重复只是第 i 个元素。复制原始向量也解释了为什么 1e5 元素比 1e4 元素慢得多,以及为什么as.list(1:1e5) 相对高效(实际上只有列表元素被复制,或者甚至没有)。调用sum 期间的重复与它属于(S3)Summary 组泛型这一事实有关,请参阅?"group generic"

【讨论】:

  • 在现代英特尔架构上,时钟周期并不是运行时的有意义指标。每个时钟周期退出的指令范围从 4 条到 1 条以下,这取决于有多少执行单元可以保持忙碌,而这又取决于代码的安排。
  • @MatthewLundberg 如果你能稍微解开你的评论,也许会有一些更好的实践指导?请随时根据需要编辑我的回复。
  • 我会查看oprofile,看看它是否可以收集 IPC 数据。英特尔的 VTune 可以,但价格相当昂贵。
  • 部分问题可能是lapply() 的非标准评估 - 它创建一个新调用然后对其进行评估。但我从来没有仔细研究过do_lapply() 以了解它在做什么。
  • @MartinMorgan 非常感谢您提供的出色细节!我期待有一些我不知道的“明显”优化标准实践。相反,似乎我确实偶然发现了一些真实的东西......你的解释提供了一个优秀的调查技巧的宝库!谢谢!
【解决方案2】:

刚加入这个东西,显然我没有足够的声誉来评论马丁的帖子,但我提交了一个补丁here来解决这个问题。实际上是两个补丁。第一个几乎在所有情况下都避免了重复。第二个更简单的补丁只是浅重复,因此对列表是重复的,但不是调用中的大向量。修复它的另一种方法是等效于 lapply(1:1e5, function(x) sum(x)),即在调用中只有符号。但是,这会干扰 do_lapply 的优化尝试,因为每次迭代都会跳过一些评估。

更新:第二个补丁已应用于 R-devel。

【讨论】:

  • 这太棒了,迈克尔!假设您继续使用 Stack Exchange(我喜欢这种格式),您很快就会有很多代表。
【解决方案3】:

这是一个奇怪的例子,因为您每次都将一个数字传递给 sumsum 可能没有针对长度为 1 的向量进行优化,而 cumsum 是。但更合乎逻辑的比较类似于

> system.time(sapply(1:1e4, function(x) sum(1:x)))
   user  system elapsed 
  0.126   0.019   0.155 
> system.time(sapply(1:1e4, function(x) cumsum(1:x)))
   user  system elapsed 
  1.601   0.158   1.824 

为了避免整数溢出,我把范围缩小了一点。

有趣的是,当你只使用两个以上的元素时,它们是多么相似

> system.time(sapply(1:1e5, function(x) sum(c(1,x))))
   user  system elapsed 
  0.196   0.001   0.204 
> system.time(sapply(1:1e5, function(x) cumsum(c(1,x))))
   user  system elapsed 
  0.170   0.005   0.188 

很明显,length(x)==1 案子发生了一些事情

【讨论】:

  • system.time(for (i in 1:1e5) sum(i)) 在我的笔记本电脑上耗时 0.07 秒。
  • 好的,很明显apply/sum 组合发生了一些事情。不一定是sum?在这种情况下,我真的应该避免使用“清楚地”这个词。
  • 我使用 microbenchmark 并对 sum(1) 和 cumsum(1) 进行了 1E5 次评估:中值 sum(1) = 458 纳秒,中值 cumsum(1) = 363 纳秒。
  • 谢谢!是的,我认识到在这种情况下 for 循环运行得更快。虽然这本身也很有趣,但我无法理解 sapply 本身的这种特殊性。
  • Doing lapply(as.list(x), sum) 运行时间为 0.146 秒,这是可以接受的,因为从 C 中调用 eval(.) 会有一些时间。查看 lapply 源应该知道为什么它当输入还不是列表时,效果很差......
猜你喜欢
  • 2018-03-14
  • 1970-01-01
  • 1970-01-01
  • 2011-08-22
  • 2011-09-18
  • 2010-10-15
  • 2021-01-16
  • 1970-01-01
相关资源
最近更新 更多