为什么在R语言中循环速度较慢?

92

我知道在R中循环很慢,所以我应该尽量使用向量化的方式来处理。

但是,为什么呢?为什么循环很慢而apply很快?apply会调用多个子函数--这看起来并不快。

更新: 对不起,这个问题问得不够清楚。我把向量化和apply混淆了。我的问题应该是:

"为什么向量化更快?"


3
我认为在R语言中,“apply比for循环快得多”的说法有点夸大其词。让我们看看回答中的system.time对比吧… - joran
1
这个主题有很多有用的信息:https://dev59.com/73E95IYBdhLWcg3wheNR - Chase
7
记录一下:Apply 不是向量化。Apply 是一个具有不同(即无)副作用的循环结构。请参见 @Chase 链接中的讨论。 - Joris Meys
4
S(或许是S-Plus)中,循环操作过去通常速度较慢。但在R中情况并非如此,因此你的问题并不相关。我不清楚S-Plus今天的情况如何。 - Gavin Simpson
5
我不明白为什么这个问题被投票反对得那么严重——对于那些从其他领域转入R的人来说,这个问题非常普遍,应该被添加到常见问题解答中。 - patrickmdnet
显示剩余5条评论
4个回答

82
循环并非总是慢的,而apply则快速。在R News的2008年5月号中有一篇关于此问题的讨论:
Uwe Ligges和John Fox. R Help Desk:我该如何避免使用循环或使其更快?R News,8(1):46-50,2008年5月。
在“循环!”这一部分(从第48页开始),他们说:
很多有关R的评论都认为使用循环是一个特别糟糕的想法。这并不一定正确。在某些情况下,编写向量化代码很困难,或者向量化代码可能会消耗大量内存。
他们进一步建议:
  • 在循环之前将新对象初始化到完整长度,而不是在循环中增加其大小。
  • 不要在循环中做可以在循环外完成的事情。
  • 不要仅仅为了避免循环而避免循环。
他们举了一个简单的例子,其中for循环需要1.3秒,但apply会耗尽内存。

74

R中的循环之所以慢,与任何解释型语言一样,是因为每个操作都需要携带大量额外的负担。

看看eval.c中调用用户定义函数时调用的R_execClosure(这是一个将近100行长的函数),它执行各种操作--为执行创建环境、将参数分配到环境中等等。

想象一下,在C中调用函数时发生了多少少事情(将参数推入堆栈、跳转、弹出参数)。

这就是为什么你会得到像这样的时间(正如joran在评论中指出的那样,实际上不是apply很快,而是mean内部的C循环很快。 apply只是普通的旧R代码):

A = matrix(as.numeric(1:100000))

使用循环:0.342秒:
system.time({
    Sum = 0
    for (i in seq_along(A)) {
        Sum = Sum + A[[i]]
    }
    Sum
})

使用sum:无法测量的微小:
sum(A)

这有点令人不安,因为在渐近意义下,循环与sum一样好;它没有实际上变慢的原因;只是每次迭代都多做了更多的额外工作。
所以请考虑:
# 0.370 seconds
system.time({
    I = 0
    while (I < 100000) {
        10
        I = I + 1
    }
})

# 0.743 seconds -- double the time just adding parentheses
system.time({
    I = 0
    while (I < 100000) {
        ((((((((((10))))))))))
        I = I + 1
    }
})

(这个例子是由Radford Neal发现的)

因为在R中,(是一个运算符,每次使用它实际上都需要进行名称查找:

> `(` = function(x) 2
> (3)
[1] 2

或者说,一般来说,解释操作(在任何语言中)需要更多的步骤。当然,这些步骤也带来了好处:你无法在C中执行那个()技巧。

11
那么最后一个例子想表达什么呢?不要在R中做愚蠢的事情并期望它能快速完成吗? - Chase
6
@Chase 我想这就是一种表述方式。是的,我的意思是像C语言这样的语言在嵌套括号方面不会有速度差异,但R语言没有优化或编译。 - Owen
1
此外,() 或循环体中的 { } -- 所有这些都涉及名称查找。或者一般来说,在 R 中写得越多,解释器就会做更多的事情。 - Owen
1
我想要明确的是,我并不是在贬低 R 语言的速度,因为它的速度与任何解释型语言相当。相反,解释型语言普遍存在“快速方式就是慢速方式”的问题,即逻辑上最小的代码仍然具有调用解释构造的开销;这意味着尽可能使用本地例程具有很强的动机。 - Owen
3
我同意你的主要观点,这是非常重要的;我们使用 R 不是因为它打破了速度记录,而是因为它易于使用且非常强大。这种强大功能会带来解释的代价。只是不清楚你在for()apply()的例子中想要展示什么。我认为你应该删除那个例子,因为虽然求和是计算平均数的一部分,但你的例子只是展示了向量化函数 mean() 的速度优势,而没有展示类似 C 风格的元素迭代的速度。 - Gavin Simpson
显示剩余4条评论

40
问题的唯一答案是:如果你需要迭代一组数据执行某些功能,并且该功能或操作未矢量化,则循环速度不会慢。一般情况下,for()循环与apply()一样快,但可能比lapply()要慢一点。最后一点在SO上有很好的涵盖,例如在此回答中,并且适用于设置和操作loop的代码是整个计算负担的重要部分的情况。
许多人认为for()循环很慢是因为他们编写了糟糕的代码。通常(虽然有几个例外),如果需要扩展/增长一个对象,那么这也将涉及复制,因此您将具有复制和增长对象的开销。这不仅限于循环,但如果您在每次循环迭代时进行复制/增长,那么循环肯定会很慢,因为您会遇到许多复制/增长操作。
在R中使用for()循环的通用习惯用法是在循环开始之前分配所需的存储空间,然后填充已分配的对象。如果遵循该习惯用法,循环将不会很慢。这就是apply()为您管理的内容,但它只是隐藏在视图中。
当然,如果存在矢量化函数来执行您使用for()循环实现的操作,请不要那样做。同样,如果存在矢量化函数(例如colMeans(foo)),请不要使用apply()等函数进行操作(例如apply(foo, 2, mean))。

9

只是为了比较(不要过分解读!):我在Chrome和IE 8中运行了一个(非常)简单的R和JavaScript for循环。 请注意,Chrome会编译成本地代码,而使用编译器包的R会编译成字节码。

# In R 2.13.1, this took 500 ms
f <- function() { sum<-0.5; for(i in 1:1000000) sum<-sum+i; sum }
system.time( f() )

# And the compiled version took 130 ms
library(compiler)
g <- cmpfun(f)
system.time( g() )

@Gavin Simpson:顺便说一下,在S-Plus中需要1162毫秒...

而在JavaScript中,这段“相同”的代码:

// In IE8, this took 282 ms
// In Chrome 14.0, this took 4 ms
function f() {
    var sum = 0.5;
    for(i=1; i<=1000000; ++i) sum = sum + i;
    return sum;
}

var start = new Date().getTime();
f();
time = new Date().getTime() - start;

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