重复子集化大型dataframe/data.table时极端耗时现象的原因探究
大型DataFrame/Data.table子集化的极端耗时现象分析
现象描述
在重复子集化一个约8GB的大型数据集(200万行×500列)时,观察到计算耗时波动极大:最大值远高于中位数,部分场景下差值甚至达一个数量级。
数据生成与基准测试代码
首先生成测试数据(修正原代码索引定义顺序的笔误):
set.seed(100) tmp_df <- data.frame(lapply(1:500, function(i) runif(2e6))) # 先定义子集索引,再生成小表子集 tmp_ind <- sample(seq_len(nrow(tmp_df)), 8000) tmp_df_2 <- tmp_df[tmp_ind, ]
对两种返回相同8000行子集的操作进行基准测试:
tmp_mbm_obj_1 <- microbenchmark(fn = tmp_df[tmp_ind, ], times = 500) tmp_mbm_obj_2 <- microbenchmark(fn = tmp_df_2[seq_len(nrow(tmp_df_2)), ], times = 500)
DataFrame基准测试结果(单位:毫秒)
| 表达式 | 最小值 | 下四分位数 | 均值 | 中位数 | 上四分位数 | 最大值 | 重复次数 |
|---|---|---|---|---|---|---|---|
| tmp_mbm_obj_1 | 69.045 | 77.8912 | 86.92647 | 81.37625 | 88.7259 | 216.2269 | 500 |
| tmp_mbm_obj_2 | 22.8655 | 26.79845 | 33.08057 | 28.666 | 31.0665 | 289.3311 | 500 |
可以看到,两种操作的最大值均远大于中位数,其中第二种操作的差值几乎达一个数量级。
Data.table下的现象
将数据转换为data.table后,均值耗时有所降低,但最大值的波动幅度更为明显:
测试代码
setDT(tmp_df) setDT(tmp_df_2) tmp_mbm_obj_dt_1 <- microbenchmark(fn = tmp_df[tmp_ind, ], times = 500) tmp_mbm_obj_dt_2 <- microbenchmark(fn = tmp_df_2[seq_len(nrow(tmp_df_2)), ], times = 500)
Data.table基准测试结果(单位:毫秒)
| 表达式 | 最小值 | 下四分位数 | 均值 | 中位数 | 上四分位数 | 最大值 | 重复次数 |
|---|---|---|---|---|---|---|---|
| tmp_mbm_obj_dt_1 | 38.1414 | 41.59365 | 50.36068 | 42.6689 | 44.5913 | 847.2804 | 500 |
| tmp_mbm_obj_dt_2 | 25.8743 | 27.56925 | 34.49421 | 27.94945 | 28.7855 | 793.3998 | 500 |
运行环境
- CPU:Xeon E5-1620 v4
- R版本:4.2.0 (2022-04-22 ucrt)
- 平台:x86_64-w64-mingw32/x64 (64-bit)
- 内存:总内存63.924 GiB,空闲内存45.379 GiB
原因分析
1. 内存缓存与页面调度
- 子集化大表时,随机抽取的行数据分布分散,CPU缓存命中率低,首次操作需要从内存中读取大量非连续数据块;而小表的连续数据更容易被缓存,平均耗时更低。
- 即使空闲内存充足,操作系统的后台进程、内存碎片整理或临时页面调度操作,会导致部分运行时缓存失效,触发较慢的内存读取,从而产生极端耗时的样本。
2. data.table的底层机制
data.table采用了更高效的内存布局和延迟加载优化,均值耗时更低,但它的内存复用策略在遇到系统临时回收缓存页时,需要重新初始化数据结构或读取数据,导致单次操作耗时大幅上升,表现为更大的最大值波动。
3. 操作系统与R内存管理
- Windows系统的mingw编译版R,对大内存数据的处理存在额外开销,当操作涉及大量内存块复制或寻址时,偶尔会触发系统级的内存整理操作,导致耗时突增。
- 尽管
microbenchmark默认会在每次运行前触发垃圾回收(GC),但仍可能存在未被捕获的GC操作,或系统后台GC抢占资源,导致部分运行耗时异常。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

