加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

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的争论,本质上涉及到两个问题:
  • 代码可读性,从这一点来说,apply胜,因为for loop的代码机械化的让你看到每一步是怎样做的,然而并不能给你整段程序在做什么的直感,apply则是在描述做什么,而不用去管怎么做。
  • 执行效率,我想这个问题才是大家真正关心的,下面就展开说说。
这里说来话有点长,首先要搞清楚小的基本问题,最后我们再综合起来讨论。
1)apply系列函数是什么?R里面有好多apply函数,比如apply,lapply,tapply至于他们到用法,我已经在别的文章里讨论过所以这里就不讨论来,如果你还没有看到,可以关注微信公众号:机器会学习(id:jiqihuixuexi)。
为什么问apply系列函数是什么呢?难道他们不显然是函数么?他们当然是函数,但是和别的函数又有区别 。对于通常的函数,比如sum函数:
class="highlight">
class="language-text">> sum(c(1,2,3))
[1] 6

输入是数值,输出也是一个数值。再看lapply函数:
class="highlight">
class="language-text">> lapply(c(1,-2,3),abs)
[[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">
class="language-text">x=data.frame(array(rnorm(10000),c(100,100)))
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。三个函数分别用了不同的方式实现:
  • fun.a是通过一个for loop 循环直接对list类型的对象y进行操作
  • fun.b也有一个for loop循环,但是这里我们是对data.frame类型的对象x进行操作
  • fun.d就不用for loop循环而用上面提到的functional,函数的函数,也就是lappy进行操作
通过micobenchmark,我们对三个函数进行了速度的比较,那么结果如何呢?fun.d会比前两个好么?
class="highlight">
class="language-text">Unit: nanoseconds
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">
class="language-text">> library(pryr)
> 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">
class="language-text">> fun.plain=function(){for(i in 1:1e6){sqrt(i)}}
> 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求根,速度提高很多。
总之,能使用向量/矩阵就用。

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有