嵌套foreach循环中data.table与data.frame的性能与结果差异问题(R)
问题描述
我正尝试优化计算列两两组合的互信息(mutinformation)和熵(entropy)的嵌套循环性能,将for循环改为foreach循环后性能有所提升但仍未达预期。测试用data.table替代data.frame后得到两个意外结果:
- data.table对性能有显著负面影响(反而更慢),希望了解优化方向及性能下降的原因;
- 两种方式的计算结果
t与t2存在差异,希望得到结果不一致的原因解释。
可复现代码
# ----- import libraries ----- libs <- c( .libPaths(), "C:/Users/her1dr/AppData/Local/TIBCO/Spotfire/114~1.0/Modules/TIBCOE~1.0/library") .libPaths(new = libs) library(infotheo) library(foreach) library(parallel) library(doParallel) library(data.table) # ----- create synthetic data set ----- input <- data.frame(replicate(150,sample(0:100,100,rep=TRUE))) # ----- set-up cluster for parallel processing ----- ncl <- max(2,floor(detectCores()*0.75)) clst <- makePSOCKcluster(ncl) e <- new.env() e$libs <- .libPaths() clusterExport(clst, "libs", envir=e) clusterEvalQ(clst, .libPaths(libs)) clusterEvalQ(clst, { #export required packages to all cores library(infotheo) library(data.table) }) registerDoParallel(cl = clst) #register cluster # ----- computed pairwise complete mutual information ----- x <- discretize(input, disc="equalwidth", nbins=20) #discretized version of the input data y <- as.data.table(x) # copy of data.frame x as data.table # -- using data.frame -- start_time <- Sys.time() t <- foreach (j=1:ncol(x), .combine="c") %:% #parallellized nested loop for computing normalized pairwise complete MI between all columns foreach (i=j:ncol(x), .combine="c", .packages="infotheo") %dopar% { compl_cases <- complete.cases(x[,c(i,j)]) if (sum(compl_cases) > 0) { 2*(natstobits(mutinformation(x[compl_cases,][,j], x[compl_cases,][,i], method="emp"))/(natstobits(entropy(x[compl_cases,][,j]))+natstobits(entropy(x[compl_cases,][,i])))) } else { 0 } } end_time <- Sys.time() print(end_time-start_time) # -- using data.table -- start_time <- Sys.time() t2 <- foreach (j=1:ncol(y), .combine="c") %:% #parallellized nested loop for computing normalized pairwise complete MI between all columns foreach (i=j:ncol(y), .combine="c", .packages="infotheo") %dopar% { compl_cases <- complete.cases(y[,.SD, .SDcols=c(i,j)]) if (sum(compl_cases) > 0) { 2*(natstobits(mutinformation(y[compl_cases,.SD, .SDcols=j], y[compl_cases,,.SD, .SDcols=i], method="emp"))/(natstobits(entropy(y[compl_cases,.SD, .SDcols=j]))+natstobits(entropy(y[compl_cases,.SD, .SDcols=i])))) } else { 0 } } end_time <- Sys.time() print(end_time-start_time) stopCluster(cl=clst) #stop cluster all.equal(t,t2)
问题解答
1. data.table性能下降的原因及优化方向
性能下降原因
.SD的不必要开销:你的data.table代码中频繁使用.SD提取列,.SD是为批量多列处理设计的,单/两列操作时会额外创建子数据表,带来对象复制和内存开销,反而比data.frame直接列索引更慢。- 并行环境的序列化成本:data.table包含键、索引等额外元数据,在PSOCK集群中传递时,序列化/反序列化的开销远大于普通data.frame,放大了节点间的数据传输成本。
- 小数据场景不匹配:data.table的优势体现在大数据量的批量操作上,而你每次仅处理2列100行的极小数据集,它的内部优化机制无法发挥作用,反而被自身封装逻辑拖慢。
优化方向
- 替换
.SD为直接列索引:取单/两列时,用y[, c(i,j), with=FALSE]替代.SD,减少额外对象创建;提取单个列时直接用y[[j]]获取向量。 - 预转成向量列表:提前将数据表的每一列转换成向量存入列表,循环时直接取列表元素,避免每次从数据表中提取列的开销:
col_list <- lapply(1:ncol(x), function(k) x[[k]]) - 调整并行任务粒度:当前每个(i,j)对单独作为一个任务,粒度太小导致调度开销过高。可以将多组(i,j)对打包成一个任务批量处理,减少调度次数。
- 预计算列熵:熵是单列属性,提前一次性计算所有列的熵并存储,避免循环中重复计算,减少冗余操作。
2. 计算结果不一致的原因
你的data.table代码存在语法错误+输入类型不匹配:
- 语法错误:
y[compl_cases,,.SD, .SDcols=i]多了一个逗号,正确语法应为y[compl_cases, .SD, .SDcols=i]。 - 输入类型错误:
.SD返回的是data.table对象,而mutinformation和entropy函数期望输入是向量(data.frame版本中x[compl_cases,][,j]返回的是向量)。当传递单列data.table给这些函数时,它们会将其当作多列数据处理,导致计算的互信息和熵值错误,最终结果与data.frame版本不一致。
修正方法:提取列时直接获取向量,比如用y[[j]][compl_cases]或y[compl_cases, get(names(y)[j])],确保传递给函数的是向量而非data.table对象。
内容的提问来源于stack exchange,提问作者MarkH
相关产品推荐
相关产品推荐

