长循环中R代码运行渐慢的原因及优化方案咨询
R代码性能劣化排查与优化
问题背景
我编写了如下R代码:
library(data.table) library(dplyr) Number = 10 iSimu = 1 dat = as.data.table(matrix(0, nr = 1, nc = Number)) while (iSimu <= 500000) { NEW = sort(sample((0 + 1 * 100) : ((100 - 1) * 100 - 1 * 100 * (Number - 1)), 10, replace = FALSE)) %>% {aa = .; cumsum(c(aa[1], diff(aa) + 1 * 100)) / 100} dat = rbindlist(list(dat, as.list(NEW))) if (iSimu %% 100000 == 0) { cat(paste0(' iSimu : ', Sys.time(), '\n')); flush.console() } iSimu = iSimu + 1 }
运行时发现代码随时间逐渐变慢,每10万次迭代的时间记录如下:
iSimu : 2025-08-24 23:46:03.370645 iSimu : 2025-08-24 23:55:13.534752 iSimu : 2025-08-25 00:19:20.071625 iSimu : 2025-08-25 01:07:38.541505 iSimu : 2025-08-25 01:48:11.571779
可见后期每10万次迭代耗时明显增加。我需要排查原因并优化代码。
我考虑过用多核心并行或lapply替代while循环,但因需执行dat = rbindlist(list(dat, as.list(NEW)))的更新逻辑(仅当NEW未存在于dat中时才更新),所以使用了while循环,此条件必须保留。
系统信息:
> Sys.info() sysname "Darwin" release "24.6.0" version "Darwin Kernel Version 24.6.0: Mon Jul 14 11:29:54 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8122" nodename "AD.local" machine "arm64"
排查与优化方案
一、性能劣化核心原因
- 内存扩容开销:每次
rbindlist都会生成新的data.table,随着dat行数持续增加,内存分配、数据复制的成本呈指数级上升,这是后期速度骤降的核心原因。 - 重复值检查的潜在开销:当前代码未实现“仅保留非重复NEW”的逻辑,直接全量追加会导致数据冗余,若后续补加判断,全表扫描的成本会随数据量增大急剧增加。
- 不必要的依赖开销:用
dplyr管道处理样本,相比原生操作存在额外的函数调用成本。
二、针对性优化方案
1. 预分配内存+批量追加
避免每次迭代都扩容,改为预分配足够容量的data.table,或每N次迭代批量追加:
library(data.table) Number = 10 total_iter = 500000 # 预分配总容量的data.table(预留冗余空间) dat = as.data.table(matrix(0, nr = total_iter + 1000, nc = Number)) row_idx = 2 # 跳过初始的第一行 for (i in 1:total_iter) { # 用base R替代dplyr管道,减少开销 sample_vals = sort(sample(100 : (9900 - 900), 10, replace = FALSE)) NEW = cumsum(c(sample_vals[1], diff(sample_vals) + 100)) / 100 dat[row_idx] = as.list(NEW) row_idx = row_idx + 1 if (i %% 100000 == 0) { cat(paste0(' iSimu : ', Sys.time(), '\n')) flush.console() } } # 清理未使用的空行 dat = dat[1:(row_idx-1)]
2. 高效实现重复值检查
用data.table的键索引加速重复值判断,避免全表扫描:
library(data.table) Number = 10 total_iter = 500000 # 初始化空表并设置全列为键 dat = data.table(matrix(0, nr = 0, nc = Number)) setnames(dat, paste0("V", 1:Number)) setkeyv(dat, names(dat)) # 批量缓存非重复行,减少频繁rbind的开销 cache = list() cache_size = 1000 for (i in 1:total_iter) { sample_vals = sort(sample(100 : (9900 - 900), 10, replace = FALSE)) NEW = cumsum(c(sample_vals[1], diff(sample_vals) + 100)) / 100 new_row = as.list(NEW) # 键查询判断是否存在 if (!dat[new_row, .N > 0]) { cache = c(cache, list(new_row)) # 缓存满时批量追加 if (length(cache) >= cache_size) { dat = rbindlist(list(dat, cache), use.names = TRUE) cache = list() } } if (i %% 100000 == 0) { cat(paste0(' iSimu : ', Sys.time(), '\n')) flush.console() } } # 追加剩余缓存 if (length(cache) > 0) { dat = rbindlist(list(dat, cache), use.names = TRUE) }
3. 移除冗余依赖
代码仅用到dplyr的管道操作,完全可以用base R替代,减少包加载和函数调用开销:
sample_vals = sort(sample((0 + 1 * 100) : ((100 - 1) * 100 - 1 * 100 * (Number - 1)), 10, replace = FALSE)) aa = sample_vals NEW = cumsum(c(aa[1], diff(aa) + 1 * 100)) / 100
4. 内存管理优化
- 定期调用
gc()回收内存,尤其是在批量操作后; - 避免在循环内创建不必要的临时变量,合并冗余的计算步骤。
三、优化效果验证
优化后重点关注:
- 内存占用是否平稳,而非持续暴涨;
- 后期迭代耗时是否与前期接近;
- 重复值过滤逻辑是否正确执行。
内容的提问来源于stack exchange,提问作者Bogaso




