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 调用,计数分配器:
| 调用 | alloc | realloc | 耗时 |
|---|---|---|---|
(0..n).into_par_iter().map(f).collect() | 1 | 0 | 0.77 ms |
...map(f).filter(|_| true).collect() | 4621–8309 | 13382–24032 | 6.29 ms |
顺序 (0..n).map(f).collect() | 1 | 0 | 1.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、或者干脆不收集。
💬 关联讨论
发起讨论加载中...