You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

长循环中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" 

排查与优化方案

一、性能劣化核心原因

  1. 内存扩容开销:每次rbindlist都会生成新的data.table,随着dat行数持续增加,内存分配、数据复制的成本呈指数级上升,这是后期速度骤降的核心原因。
  2. 重复值检查的潜在开销:当前代码未实现“仅保留非重复NEW”的逻辑,直接全量追加会导致数据冗余,若后续补加判断,全表扫描的成本会随数据量增大急剧增加。
  3. 不必要的依赖开销:用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

火山引擎 最新活动