为何furrr::future_pmap()始终慢于pmap()?如何优化?
future_pmap()始终慢于pmap()?排查与优化方案 实现误区排查
purrr::pmap不受future计划影响
如果你调用的是purrr原生的pmap,那么plan(multicore)不会让它变成并行执行——这个函数是纯串行的单线程实现。而future_pmap(来自future.apply或furrr)才是基于future框架的并行版本。如果你的pmap实际串行运行却比并行的future_pmap快,核心原因几乎都是并行开销超过了并行收益。计时逻辑需统一
你的future_pmap代码块没有将结果赋值(比如result <- future_pmap(...)),虽然顶层调用会强制求值,但某些场景下惰性求值可能导致计时不准。建议统一为所有测试代码添加结果赋值,确保完整执行所有计算后再统计耗时。
性能差异核心原因
任务粒度太小,并行开销占比过高
你最后测试的grid有10901000行,意味着要执行1000多万个极轻量任务(每个任务只是sum(v,x,y,z))。future_pmap的并行执行需要额外开销:
- 进程间的任务调度、通信
- 参数与结果的序列化/反序列化
这些开销累加后,会远远超过并行计算节省的时间,导致总耗时反而比串行的pmap更长。
future框架的抽象开销
future为了支持多场景并行(多核、集群、云等),引入了额外的抽象层,这会比底层并行工具(如parallel包)带来更多开销。对于轻量任务,这种开销的影响会被进一步放大。
优化方案
1. 增大任务粒度(最有效)
将大量小任务合并为少量大任务,减少任务总数,降低开销占比。比如按worker数量拆分grid,每个批次处理多行数据:
library(future.apply) plan(multicore, workers = 4) # 拆分grid为4个批次,对应4个worker grid_batches <- split(grid, rep(1:4, length.out = nrow(grid))) # 批量处理每个批次 result <- future_lapply(grid_batches, function(batch) { apply(batch, 1, function(row) sum(row)) }) # 合并最终结果 final_result <- unlist(result) plan(sequential)
这样每个worker处理几十万到几百万行的批量任务,并行收益会远超过开销。
2. 调整future参数减少开销
- 启用
lazy = FALSE:plan(multicore, workers = 4, lazy = FALSE),避免惰性求值带来的额外调度开销 - 关闭随机种子传递:设置
future.seed = FALSE(如果不需要固定随机种子),减少种子同步的开销 - 使用类型特定的
future_pmap变体:比如future_pmap_dbl(针对数值结果),避免自动类型转换的开销
3. 换用更适合轻量并行的工具
如果任务都是这类极轻量的数值计算,data.table结合parallel包的原生并行,或者Rcpp实现会比future_pmap更高效。例如:
library(data.table) library(parallel) cl <- makeCluster(4) result <- parApply(cl, grid, 1, sum) stopCluster(cl)
验证并行是否生效
可以在函数中加入进程ID打印,确认future_pmap确实在使用多核:
future_pmap(list(grid[,1],grid[,2],grid[,3],grid[,4]),.f=function(v,x,y,z){ cat("Worker PID:", Sys.getpid(), "\n") return(sum(v,x,y,z)) })
如果输出的PID只有一个,说明并行未生效,需要检查plan设置和包的加载情况(比如是否正确加载了future.apply或furrr)。
内容的提问来源于stack exchange,提问作者mexx

