为何data.table中fifelse比子集查询更新列的速度更快?
为什么data.table中用fifelse的列更新比子集筛选更快?
我在比较data.table中两种更新列的方法的速度差异(测试用flag列50%为TRUE、50%为FALSE):
方法1(子集筛选更新):
A[flag==TRUE,b:=b + myfunc(a,b)]
方法2(fifelse全列更新):
A[,b:=b + fifelse(flag,myfunc(a,b),0)]
我原本认为第一种方法更快,因为二者对flag列的判断次数、计算量相同,但第二种方法存在多余的b+0计算和全列b:=b重新赋值。然而测试结果显示第二种方法快了近24%:
[1] "Total time run 1: 0.281515121459961" [1] "Total time run 2: 0.227149963378906"
完整测试代码:
library(data.table) n = 10000000 # 自定义非 trivial 函数 myfunc= function(x,y) { return((x+y)*x/(y^2)) } A = data.table(flag=rep(c(TRUE,FALSE),n/2),a=(1:n)/n, b = (1:n)/n) run1Start = Sys.time() A[flag==TRUE,b:=b + myfunc(a,b)] run1End = Sys.time() print(paste0("Total time run 1: ", difftime(run1End,run1Start,units="secs"))) A = data.table(flag=rep(c(TRUE,FALSE),n/2),a=(1:n)/n, b = (1:n)/n) run2Start = Sys.time() A[,b:=b + fifelse(flag,myfunc(a,b),0)] run2End = Sys.time() print(paste0("Total time run 2: ", difftime(run2End,run2Start,units="secs")))
核心原因:子集筛选的额外成本,比全列向量化的多余操作高得多
子集筛选藏着看不见的开销
方法1里A[flag==TRUE, ...]看似只处理一半数据,但实际要做这些事:- 先算出哪些行满足
flag==TRUE的布尔索引 - 基于这个索引定位要更新的行,相当于给数据做了一次“拆分”
- 对拆分后的子集做计算,再把结果合并回原表
千万级数据量下,这一系列定位、拆分、合并的操作,消耗的时间比你想象的多得多,尤其是当子集占比达到50%时,这种开销会被放大。
- 先算出哪些行满足
fifelse是底层优化的向量化工具
fifelse是data.table专门做的、底层用C语言实现的向量化函数,它会直接对整个列从头到尾做一次循环计算,全程在C层面完成,不需要在R和C之间来回切换。而方法1的子集更新,需要在R层面处理筛选逻辑,再调用C做计算,中间的切换成本很高。你担心的"多余操作"其实几乎不耗时
你觉得b+0和全列赋值是浪费,但实际上:b+0是向量化的简单算术操作,C层面一次循环就搞定,耗时可以忽略- data.table的
:=是原地修改数据,全列更新只是覆盖原有值,不需要额外的内存拷贝,这个过程比筛选后更新子集的成本低很多。
额外说明:子集比例不同结果会变
如果flag为TRUE的比例特别低(比如只有1%),那方法1肯定更快,因为此时筛选的开销很小,而全列计算的多余操作占比高。但在你的测试场景里(一半数据要更新),全列向量化的效率优势完全盖过了那些看似多余的操作。
内容的提问来源于stack exchange,提问作者Sinnombre
相关产品推荐
相关产品推荐

