Rust in Everything
基于 rayon 1.12.0

filter 之后 collect 慢了 8 倍

主线位置:主线 2 的终点换成 collect。两条路径的代码在 collect 到 Vec。

flowchart TB
  col["collect::<Vec<T>>()"] --> q{"opt_len()"}
  q -->|"Some(len):map、zip、enumerate"| fast["reserve 一次,各叶子写不相交区间"]
  q -->|"None:filter、flat_map"| slow["每个叶子一个 Vec 从空增长"]
  slow --> list["拼成 LinkedList<Vec<T>>"]
  list --> copy["reserve 总长,逐个 append:再拷贝一次"]

现象

v.par_iter().map(f).collect::<Vec<_>>() 很快;加一个 .filter(..) 之后,哪怕过滤条件几乎都成立,耗时成倍增长,分配器的调用次数从个位数涨到几千、上万,比顺序的 iter().map(f).filter(..).collect() 还慢。

复现

实验 E8(exp-rayon/collect-paths/),n = 10⁷ 个 u64(map 一个乘法),12 线程,从 main 调用,计数分配器:

调用allocrealloc耗时
(0..n).into_par_iter().map(f).collect()100.77 ms
...map(f).filter(|_| true).collect()4621–830913382–240326.29 ms
顺序 (0..n).map(f).collect()101.38 ms

(耗时是 7 次中位数,1 / 5 分钟负载 1.97 / 2.98;分配次数跨八次运行。)filter(|_| true) 一个元素都没过滤掉,耗时却是 indexed 链的 8 倍、顺序版的 4.5 倍。collect_vec_list() 省掉最后一次拷贝,分配次数同量级(collect 到 Vec)。

从哪来

Vec::par_extend 按 opt_len() 分两路:

  • 有长度(indexed 链:map、zip、enumerate、rev……):special_extend,一次 reserve(len),每个叶子把元素直接写进目标 Vec 里属于自己的那一段,最后 set_len。
  • 没有长度(filter、filter_map、flat_map、par_bridge 等之后):ListVecConsumer,每个叶子一个从空开始增长的 Vec(filter 的 folder 是逐个 push),回溯时拼成 LinkedList<Vec<T>>,最后 reserve 总长再逐个 append,所有元素多拷贝一次。

叶子数由切分树决定,从池外调用、有窃取时 10⁷ 个元素是几千个(bridge 与自适应切分),所以是几千个 Vec 各自翻倍增长。filter 之后长度未知,这一条没法靠 rayon 内部避免:没有特化,par_extend 只能用 opt_len() 判断。

怎么改

  • 先 map 成 Option,最后再过滤:并行部分保持 indexed,v.par_iter().map(|x| keep(x).then(|| f(x))).collect::<Vec<Option<_>>>(),再顺序 into_iter().flatten().collect()。只在过滤掉的比例不大时划算,要实测。
  • 不需要一个连续的 Vec:collect_vec_list() 直接拿 LinkedList<Vec<T>>,下游逐块处理,省一次整体拷贝。
  • 能在并行里做完的就别收集:filter(..).map(..).sum()、.count()、.for_each(..) 都不需要中间 Vec。
  • 结果写进预分配的缓冲:知道上界时,par_chunks_mut 每块写自己的部分并返回有效长度,最后顺序压紧。
  • 用 with_min_len 减少叶子数能降低分配次数(叶子少 → Vec 少),但最后那次拷贝还在。

小结

  • filter / filter_map / flat_map 之后 opt_len() 为 None,collect 走 LinkedList<Vec<T>>:每个叶子一个增长的 Vec,最后再拷贝一次。
  • E8:10⁷ 个元素 indexed 1 次分配 0.77 ms,filter(|_| true) 几千次分配加一两万次 realloc,6.29 ms,比顺序版还慢。
  • 改法:保持 indexed(map 成 Option 再顺序 flatten)、用 collect_vec_list、或者干脆不收集。

💬 关联讨论

发起讨论
加载中...