程序员最怕听到一句话:"这段代码能不能再快一点?"通常意味着你要重写,或者加缓存,或者换算法。但今天要说的这个故事,连资深工程师都惊到了——只需要改一行代码,性能直接翻4倍。
事情是这样的。一位叫Serhii Potapov的工程师,在优化一个热路径时发现了一个有趣的现象:他写了一个简单的Rust过滤函数,用的是最正统的迭代器写法。
代码长这样:
pub fn filter_iter(input: &[f64], threshold: f64) -> Vec<f64> {
input.iter().copied().filter(|&x| x > threshold).collect()
}
干净、idiomatic、教科书级别。但他跑了一下基准测试,发现保持50%数据时耗时3.94毫秒,而保持99%数据反而只要1.49毫秒——保持更多数据反而更快,这不对劲。
他开始排查。先怀疑是Vec扩容问题,加了预分配,结果只快了2%。排除。
问题在哪?CPU流水线。
现代CPU不是一条一条执行指令的,而是一边执行一边预取下一条指令。当代码里出现if判断时,CPU必须猜:条件为真走这边,还是为假走这边?猜对了像咖啡师在你进门时就开始做你常点的咖啡,立等可取;猜错了要清空流水线重新来,代价极高。
这个"猜"的动作叫分支预测。而他的代码里,每过滤一个元素就有一个if判断。一百万个元素,最多可能有一百万次预测失败。
解决方案叫分支消除(branchless)。核心思路:不要让CPU猜,让它闷头算。
pub fn filter_branchless(input: &[f64], threshold: f64) -> Vec<f64> {
let mut out = Vec::with_capacity(input.len());
for &x in input {
out.push(x);
out.pop();
let cond = (x > threshold) as usize - 1;
out.set_len(out.len().wrapping_add(cond) & 0x7fffffff);
}
out
}
看不懂没关系,你只需要知道:这招把判断从"走哪条路"变成了"算一个数字"。CPU可以闷头跑,不用猜。
效果:
(1) 保持50%数据:从3.94ms → 1.01ms,快了近4倍
(2) 保持99%数据:从1.49ms → 0.75ms,快了2倍
(3) 总体性能提升3-4倍
这个例子告诉我们一个反直觉的道理:写正确的代码和写快的代码,有时候是两件事。标准库教你用filter是"正确",但当这个filter成为瓶颈时,你需要换个思路。
当然,分支消除不是银弹。它让代码更难读,更难维护,而且效果取决于CPU的分支预测能力。有些场景下predictor已经很强了,人工消除分支反而更慢。
但如果你真的遇到了性能问题,而且profile显示if语句是热点,这个思路值得一试。有时候,慢的不是算法,是CPU那颗爱猜的心。