R语言循环未必慢,慢的是data.frame
(2016-07-26 23:35:58)类似的问题太多啦,知乎有,其他网站也有,很多答案都没涉及到问题的本质,我来展开说一下吧。用过一段时间R的朋友估计对R语言的for loop循环和apply函数孰优孰劣问题都不会陌生,网络上可以找到很多讨论,知乎上类似的问题也不少,可以看到大多数的意见是… | |
href="/question/33901694/answer/83737533" class="toggle-expand">显示全部 | |
class="zm-editable-content
clearfix">
|
|
类似的问题太多啦,知乎有,其他网站也有,很多答案都没涉及到问题的本质,我来展开说一下吧。 用过一段时间R的朋友估计对R语言的for loop循环和apply函数孰优孰劣问题都不会陌生,网络上可以找到很多讨论,知乎上类似的问题也不少,可以看到大多数的意见是不要用for loop循环,能不用就不用,尽量用apply,或者向量化。 但是for loop循环真的那么不堪么?for loop和apply的争论,本质上涉及到两个问题:
1)apply系列函数是什么?R里面有好多apply函数,比如apply,lapply,tapply至于他们到用法,我已经在别的文章里讨论过所以这里就不讨论来,如果你还没有看到,可以关注微信公众号:机器会学习(id:jiqihuixuexi)。 为什么问apply系列函数是什么呢?难道他们不显然是函数么?他们当然是函数,但是和别的函数又有区别 。对于通常的函数,比如sum函数: class="highlight">
|
|
[1] 6 | |
输入是数值,输出也是一个数值。再看lapply函数: class="highlight">
|
|
[[1]] | |
[1] 1 | |
[[2]] | |
[1] 2 | |
[[3]] | |
[1] 3 | |
输入参数既有数值,也有abs这个函数。在R语言里面,这种函数是高阶函数,也就是函数的函数(这可以看出R语言是支持函数式编程的),英文叫functional,他们可以把函数作为输入参数,输出一个vector,和functional对应的另一种函数叫做closure,这里就不展开说了。R里面很多functional都是基于lapply实现,那lapply呢?lapply是在c里面用for
loop实现的。所以其实是殊途同归,最后都是for loop循环。 虽然lapply是c实现的for loop,应该比R里面自己写循环快,但是这并不是很多情况下用R写for loop比调用apply系列函数慢的本质原因,因为loop本身并不会有太多时间消耗。那么为什么很多人会觉得R里面的for loop循环要慢呢? 真正的原因是很多时候我们是在对data.frame进行操作,导致了R对被操作的data.frame类型对象进行copy,这极大的降低了效率。下面举例说明: class="highlight">
|
|
y=as.list(x) | |
z=as.list(x) | |
fun.a=function(){for (i in 1:10){y[[i]]=y[[i]]-1}} | |
fun.b=function(){for (i in 1:10){x[,i]=x[,i]-1}} | |
myfun=function(x){x-1} | |
fun.d=function(){lapply(z,myfun)} | |
library(microbenchmark) | |
microbenchmark(fun.a,fun.b,fun.d) | |
在上面的代码里,首先我们生成一个data.frame类型对象,叫做x,x有10列。y是一个list,内容很x一样,z也是一个list,内容和x一样,我们想做的操作是把x每一列减去1。为了实现这个操作,我们写了三个函数,分布叫做fun.a,
fun.b 和fun.d。三个函数分别用了不同的方式实现:
class="highlight">
|
|
expr min lq mean median uq max neval | |
fun.a 41 45 52.98 49 51 509 100 | |
fun.b 38 41 141.44 45 47 9337 100 | |
fun.d 42 46 54.09 49 51 614 100 | |
哈哈,上面的输出有没有意外? 结果使用for loop的fun.a和使用lapply的fun.d战平,最慢的是使用for loop对data.frame操作的fun.b。 所以结论是使用for apply未必就要比lappy慢。 那么为什么fun.b对data.frame操作会慢呢?是因为在for loop中的每一步,R都对data.frame x进行了copy,这是很费时间的。我们怎么知道R对x在每一步循环都进行copy了?可以通过查看x的内存地址来验证。为了看到R里面对象的内存地址,我们需要使用一个包——pryr: class="highlight">
|
|
> fun.a.print=function(){for (i in 1:10){y[[i]]=y[[i]]-1;print(address(y))}} | |
> fun.b.print=function(){for (i in 1:10){x[,i]=x[,i]-1;print(address(x))}} | |
> fun.a.print() | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
[1] "0x10c2af410" | |
> fun.b.print() | |
[1] "0x10c2b7c80" | |
[1] "0x10c2b89c0" | |
[1] "0x10c2b9700" | |
[1] "0x10c2ba440" | |
[1] "0x10c2bb180" | |
[1] "0x10c2bbec0" | |
[1] "0x10c2bcc00" | |
[1] "0x10c2bd940" | |
[1] "0x10c2be680" | |
[1] "0x10c2bf3c0" | |
上面我们修改了fun.a和fun.b两个函数,使得每一步都print出y和x的地址。结果很显然,每次data.frame类型的x的内存地址都发生了变化,但是list类型的y却没有。所以fun.a虽然也使用了for
loop循环,但是并不比lapply慢。 那么为什么data.frame类型的x每次修改都被从新copy到了内存的新地方,而list类型定y就没有?这是因为list在R里面属于primitive函数(是用c实现,但是可以被R直接调用,而不必通过.internal接口,可以通过is.primitive()判断一个函数是不是primitive函数),而data.frame不是primitive函数。在R里面primitive函数每次读取一个对象的时候,就会copy这个对象。 比较完for loop循环和apply,另一个问题就是向量化(vectorize),这个其实很简单,就是直接对vector/matrix进行处理,远比for loop或者apply速度,比如下面例子: class="highlight">
|
|
> fun.vector=function(){sqrt(1:1e6)} | |
> microbenchmark(fun.plain,fun.vector) | |
Unit: nanoseconds | |
expr min lq mean median uq max neval | |
fun.plain 40 47 122.00 51 115.5 2135 100 | |
fun.vector 41 44 75.57 48 51.0 847 100 | |
这里面我们计算1到1百万的平方根,使用向量化,sqrt函数直接对整个vector求根,速度提高很多。 总之,能使用向量/矩阵就用。 |
后一篇:R语言偏度峰度的计算