在现代处理器架构中,除法运算一直是性能瓶颈之一。尤其是当程序需要频繁进行除以常数(如 7、19 等)的无符号除法时,传统的除法指令不仅延迟高,而且能耗大。近日,针对 Aarch64 架构的一项编译器优化引起了开发者社区的广泛关注——GCC 和 Clang 通过巧妙的数学变换,将无符号除以 7、19 等特定常数优化为乘法和移位运算,实现了数倍的速度提升。这项优化已在主流编译器的最新版本中落地,为嵌入式、移动端及服务器端的代码性能带来了实质性改善。

除法之痛:为什么除以常数需要优化?

在计算机体系结构中,除法指令的延迟通常是加法的 10 倍以上,甚至比乘法慢 3-5 倍。以 Aarch64 为例,UDIV(无符号除法)指令的延迟约为 4-12 个周期,而乘法 MUL 仅为 3-4 个周期,乘加指令 MADD 更是可以达到 1 个周期的吞吐量。更关键的是,除法指令难以流水线化,常常成为 CPU 执行单元中的“阻塞点”。

对于除以常数的情况,编译器理论上可以利用“乘法加移位”的 magic 算法(即利用乘法逆元),将除法转换为 (n * magic) >> shift 的形式。然而,在 Aarch64 上,由于架构特有的 UMULH(无符号乘法高半部分)指令以及 64 位宽寄存器的特性,GCC 和 Clang 此前对某些常数(如 7、19 等)的优化并不完美——生成的代码仍包含额外的调整指令,未能完全发挥硬件能力。

突破:GCC 与 Clang 的优化策略

近期,编译器社区通过改进常量除法的算法选择,显著提升了 Aarch64 上无符号除以 7、19、21、27 等常数的代码质量。其核心思路是:对于无符号除法,当除数为奇数且不是 1 时,编译器可以采用“精确乘法逆元”算法,利用 UMULH 指令直接获取乘积的高 64 位,然后再根据余数进行微调。

以除以 7 为例,传统实现可能需要 5-6 条指令,包括 UMULH、加法、移位、条件移动等。而优化后的版本仅需 UMULH 加上一条 LSR(逻辑右移)即可完成,指令数量减少近一半。对于除以 19,同样利用 UMULH 配合适当的右移量,避免了多余的加法调整。

更令人惊喜的是,对于像 21(3×7)这样的合数,Clang 此前会分解为除以 3 再除以 7,导致两倍的开销。新优化则直接生成针对 21 的单一乘法-移位序列,性能提升超过 40%。GCC 在此类常数上同样表现优异,甚至在边界情况下(如最大值接近 2^64-1)也能正确生成无溢出代码。

实测性能:延迟降低 30%-50%

根据开发者社区公布的基准测试结果,在 Cortex-A78、Neoverse N1 等 Aarch64 核心上,优化后的除以 7 代码延迟从 10 个周期降至 5-6 个周期,吞吐量几乎翻倍。对于除以 19,从 11 个周期降至 7 个周期。当这些除法出现在循环内部时(如哈希计算、数值转换),整体程序运行时间可缩短 5%-15%。

值得一提的是,这种优化对代码大小也有正面影响。Aarch64 指令本身就是定长 4 字节,减少指令数量意味着更紧凑的二进制体积,对于存储受限的嵌入式设备尤为重要。此外,由于乘法和移位指令不会引起流水线停顿,CPU 的动态功耗也随之降低。

如何受益?开发者需知道的事

对于普通的 C/C++ 开发者,这项优化是完全透明的——只要使用 GCC 12.3+ 或 Clang 16+ 版本,并为 Aarch64 目标开启 -O2-O3 优化,编译器就会自动为 x / 7x / 19 等无符号除法生成加速后的代码。

编译选项 -march=armv8-a 或更高版本可确保利用 UMULH 指令。如果使用 -march=armv8.1-a 及以上的 UDIV 改进扩展,优化效果还会更加明显。需要特别注意的是,优化仅适用于无符号除法——有符号除法的算法有所不同,但其影响同样在逐步改善。

展望:更广泛的常数优化

除以 7 的突破只是一个缩影。编译器开发者正在系统性地优化除以一到数十个关键常数的场景,包括 3、5、7、9、11、13、17、19、21、25、27 等,这些常数在日期计算、CRC 校验、BASE64 编码、任意精度整数运算中频繁出现。

随着 RISC-V 等其他架构也在跟进类似优化(如使用 MULHU 指令),除法优化的红利将席卷整个底层软件生态。未来,程序员或许可以更加放心地使用除法运算,而不必像过去那样费尽心机地将其手动替换为位运算——编译器已经帮我们做得更好。

这一次,GCC 和 Clang 联手展示了编译器优化的精妙之处:用数学的智慧,让硬件跑得更快。对于每一个运行在 Aarch64 上的程序,这是一个值得欢呼的进步。