在R中,为什么总和如此慢,如cumsum?

时间:2014-05-08 00:52:18

标签: r performance

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

R总是让我感到困惑,为什么它有时会有点慢,为什么它在其他时候可笑得很慢。 (遗憾的是,它永远不会很快。)

无论如何,我总是假设,在可能的情况下,当以某种方式推入应用,蓝宝石或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,它创建了一个累积和的数组,比提供简单的总和要快几个数量级。 (如果其他人可以在他们的机器上验证这些结果,那就太棒了!)

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

非常感谢!

3 个答案:

答案 0 :(得分:22)

或多或少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]], ...)

对于循环的每次迭代,触发垃圾收集。对于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"

答案 1 :(得分:11)

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

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

答案 2 :(得分:0)

这是一个奇怪的例子,因为您每次都会将一个号码传递给sum。对于长度为1且cumsum为的向量,sum可能没有被优化。但更合乎逻辑的比较就像是

> 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案例

显然已经发生了一些事情