只改了几行写法,排序突然快了六倍:这不是玄学,是编译器
分类:编程与性能 Tag:C、Clang、GCC、快速排序、编译器优化、分支预测 作者:周白|OC 编辑
分类:编程与性能
Tag:C、Clang、GCC、快速排序、编译器优化、分支预测
作者:周白|OC 编辑
开发者 Christof Kaser 在文章 Lucky Code 中记录了一次很有意思的性能实验:两段逻辑等价的 C 代码,只因为指针递增写法不同,Clang 生成的机器码就发生变化,5000 万个 double 的排序时间从 4.39 秒降到约 0.70 秒。
一句话结论:这不是一条“把 if 写短就会变快”的通用技巧,而是一个典型提醒:在性能关键代码里,源代码语义相同,不代表编译器一定生成相同指令。
变化看起来很小。原始代码先赋值,再单独移动指针;改写后把 *lwr++ = x 放在一行。作者观察到,Clang 更容易把后者转换成条件选择指令,减少难以预测的分支。快速排序的分区过程要重复执行大量比较,一旦数据让分支预测频繁失败,CPU 流水线就会不断清空,代价被放大。

但标题里的“六倍”不能脱离环境。测试使用 Apple M1、Clang -O3、特定数据分布和作者自写的 branchless quicksort;同一版本在 GCC 上没有出现完全相同的优化,C++ std::sort 的对照成绩也依赖标准库实现。换一台 CPU、编译器版本、数组大小或数据重复率,结果可能完全不同。
真正可复用的方法不是背下那一行代码,而是查看生成的汇编并建立基准。先确认瓶颈是否真是分支,再用 perf、Instruments 或硬件计数器观察 branch miss;之后对比编译器输出,最后在真实数据上测量。没有这些步骤,“更紧凑的写法更快”只是碰巧成立的故事。
这也解释了为什么成熟库的微优化常常看起来古怪。它们不是为了写得漂亮,而是在和具体编译器、具体架构谈判。只是这种谈判会随版本改变,所以必须有基准测试守着,不能把一次偶然的代码生成当成永恒规则。
如果想复现实验,至少要固定编译器完整版本、优化参数、CPU 型号和数据生成方式,并在排序后校验结果,避免“更快”其实来自未执行完整逻辑。基准还应预热缓存、重复运行多次并报告分布,而不是只挑最快的一次。把两版函数放进 Compiler Explorer 查看汇编,只是定位原因的开始。
还要区分吞吐提升和用户体验提升。排序内核快六倍,如果它只占应用总耗时的 5%,按照 Amdahl 定律,整个程序最多也只会快一点点。相反,如果这是数据库、压缩库或实时交易系统每天执行数十亿次的热路径,几条指令的差异就可能值得专门维护一套基准和架构分支。
关键事实
- 作者报告排序 5000 万个 double 的耗时从 4.39 秒降至约 0.70 秒。
- 优化与 Clang 生成条件选择、减少不可预测分支有关。
- GCC 没有表现出完全相同的代码生成变化。
- 所有数字来自作者环境,不能直接推广到其他硬件和数据。
OC 判断
性能优化首先是测量工作,其次才是代码技巧。能进入生产代码的结论,至少要同时回答:快在哪里、为什么快、换数据还快不快、升级编译器后是否仍然成立。
为什么重要
- 对开发者:检查汇编和硬件计数器,比凭源代码外观猜性能可靠得多。
- 对团队:关键路径应保留可重复基准,防止编译器升级带来隐性回退。
- 对普通用户:所谓“几行代码提升六倍”通常有严格适用条件,不等于整个应用会快六倍。
评论
围绕这篇文章补充信息、提出问题或分享观察。