Hotdry.

Article

32位无符号除以常数在64位目标上的优化:单指令乘法替代三段移位

深入解析 Granlund-Montgomery 方法在 33 位常数场景下的优化,通过单条乘法指令实现 32 位无符号除法,显著提升 64 位 CPU 性能。

2026-04-13compilers

在现代编译器后端优化中,整数除法作为最耗时的基础运算之一,长期以来是性能优化的重点领域。当除数已知为常数时,编译器可以通过预先计算的「魔法常数」将除法转换为乘法与移位的组合,从而大幅提升执行效率。然而,针对 32 位无符号整数除以常数在 64 位目标平台上的优化策略,长期以来存在一个被忽视的性能瓶颈 —— 即当魔法常数需要 33 位表示时,传统方法会产生冗余的三段移位序列。本文将详细解析这一问题的技术本质,并给出基于最新编译器实现的优化参数与监控要点。

问题背景:除法常数的编译器优化机制

整数除法在 CPU 微架构中属于高延迟运算。以 Intel Sapphire Rapids 架构为例,整数除法的延迟可达 12 至 30 个周期,而乘法仅需 3 至 4 个周期。基于这一性能差异,编译器普遍采用 Granlund-Montgomery(GM)方法将除法转换为等价的乘法与移位操作。该方法的核心定理表明:对于给定的除数 d 和最大可表示无符号整数 M(对于 32 位整数,M = 2^32 - 1),存在一个常数 c 和移位量 a,使得对于所有 x ∈ [0, M],都有 x /d = floor ((x × c) / 2^a) 成立。

这一转换的理论基础在于:有理数近似技术可以在有限精度的整数域中找到 1/d 的良好近似值 c / 2^a,从而将除法运算替换为一次乘法和一次右移位。在实际编译器实现中,c 被称为「魔法常数」(magic number),a 被称为移位量。根据 1994 年 Granlund 和 Montgomery 的原始论文,当 M 为 32 位整数时,c 最多只需要 33 位即可满足所有除数 d 的优化需求。统计数据表明,在所有小于 0x80000000 的非平凡除数中,约 77% 的情况可以用 32 位魔法常数处理,剩余约 23% 需要 33 位表示。

传统实现的性能瓶颈分析

当魔法常数 c 需要 33 位表示时,传统编译器生成的代码采用三段移位序列来实现等效计算。具体而言,对于除数 d = 7,编译器会生成如下 C 代码形式的等价实现:首先计算 y = (x × c_L) >> 32,其中 c_L 是常数 c 的低 32 位;然后计算 t = ((x - y) >> 1) + y;最后返回 t >> (a - 33)。这种实现方式的设计约束是确保所有中间值都保持在 32 位整数范围内,避免使用 64 位或 128 位寄存器带来的额外开销。

然而,这一约束在 64 位 CPU 上实际上是过于保守的。64 位架构天然支持 64 位寄存器和 64 位算术运算,完全可以在不溢出前提下一次性完成乘法与移位。以 x86-64 架构为例,传统实现需要执行:一次 32 位乘法、一次 32 位右移、一次减法、一次右移、一次加法、最后一次右移,共六个微操作。而在 Apple M4(AArch64)上,传统实现同样需要 umull、lsr、sub、add、lsr 等多条指令的序列。

更关键的性能问题在于,128 位逻辑右移指令 shrd 在 Intel Skylake-X 架构上的延迟与乘法 mul 指令相当,这意味着传统方法并未充分利用 64 位架构提供的宽寄存器优势。在循环中重复执行此类除法时,冗余的移位操作会显著累积成为性能瓶颈。

单指令乘法优化方案

针对上述问题,最新研究提出了针对 64 位目标平台的优化方法。其核心思想是利用 64 位寄存器的宽度,直接计算 (x × c) >> a,而不再将计算拆分为多个 32 位操作。关键变换公式如下:设 c 为 33 位魔法常数,a 为对应的移位量(33 ≤ a ≤ 63),则 (x × c) / 2^a 可以等价变换为 (x × (2^(64-a) × c)) / 2^64。由于 2^(64-a) × c 的结果必然落在 64 位范围内(因为 33 ≤ a ≤ 63),该乘法可以一次性使用 64 位 × 64 位 → 128 位乘法指令完成,而右移 64 位相当于直接提取 128 位结果的高 64 位。

在 x86-64 架构上,该优化使用 mulx 指令实现。mulx 是一种 BMI2 扩展指令,能够执行无符号乘法并将结果存储到指定的两个寄存器中(低 64 位和高 64 位)。优化后的代码仅需两条指令:先将移位后的魔法常数加载到寄存器,然后执行 mulx 乘法并直接从 rdx(高 64 位)获取结果。以除数 7 为例,优化后的 x86-64 汇编仅需 movabsq 加载常数和 mulx 执行乘法,循环体从原来的约 20 条指令缩减为约 10 条。

在 Apple M4(ARM64)架构上,该优化利用 umulh 指令。umulh 是无符号乘法高 64 位指令,直接返回 64 位 × 64 位无符号乘法结果的高 64 位。优化后的代码同样只需两条指令:加载常数和执行 umulh。与传统方法相比,umulh 指令的延迟与乘法相当,但省去了多次移位和加法操作的开销。

实际性能提升与基准测试

该优化方法已实现为 LLVM 编译器补丁并合并到 llvm:main 主干,同时提供了对应的 GCC 补丁供审查和测试。基准测试采用 32 位无符号整数除以常数 7、19 和 107 的循环场景,这三个除数均属于需要 33 位魔法常数的典型案例。

测试结果在 Intel Xeon w9-3495X( Sapphire Rapids)上显示:优化前平均运行时间 6.33 秒,优化后缩短至 3.80 秒,提升比例达到 1.67 倍。在 Apple M4 MacBook Pro 上,提升更为显著:优化前平均运行时间 6.70 秒,优化后缩短至 3.38 秒,提升比例达到 1.98 倍。两组测试的标准差分别为 0.013 秒和 0.009 秒,表明测量结果具有很高的稳定性。

从生成的汇编代码可以直观看到优化效果。以 x86-64 平台为例,传统方法生成的循环体包含多次显式的移位指令(shrq)和加法指令(addl),而优化后仅包含 mulx 乘法指令和必要的寄存器操作。Apple M4 平台的变化更为明显:优化前每个除法需要 umull、lsr、sub、add 等 5 条以上指令,优化后每个除法仅需一条 umulh 指令即可完成。

工程实践中的监控与配置要点

在生产环境中部署该优化时,开发者需要关注以下几个关键配置点。首先,确保编译器版本支持该优化特性。Clang 18.1.3 及以上版本、LLVM 23.0.0 之后的构建版本已包含该优化;对于 GCC,需要使用支持对应补丁的构建版本或等待正式合并。在编译选项方面,x86-64 目标应启用 -mbmi2 以支持 mulx 指令,AArch64 目标则需要 ARMv8.0-a 或更高架构版本。

监控层面建议关注以下指标:单个除法操作的平均周期数(可通过硬件性能计数器 PAPI 或 vtune 采集)、循环体内指令总数变化、以及因除法优化带来的整体 IPC(每周期指令数)提升。对于高频执行除法操作的代码路径(如哈希表再散列、像素坐标转换、时频变换等场景),优化收益尤为显著。

在某些特殊场景下,传统方法可能仍然适用或更优。当除数 d 为 2 的幂时,应直接使用右移位操作,这是所有编译器的标准优化。当除数 d > M/2(即除数大于 2^31)时,商的值域仅为 {0, 1},使用条件选择指令(如 cmov)可能比乘法方案更高效。此外,在不支持 BMI2 扩展的旧版 x86-64 处理器上,mulx 指令不可用,需要回退到传统实现。

总结与展望

32 位无符号除法常数的优化是编译器后端的一个重要技术细节。在 64 位目标平台上,针对 33 位魔法常数的单指令乘法优化能够带来接近 2 倍的性能提升,这一改进已在 LLVM 编译器中实现并合并。对于性能敏感的应用程序开发者,建议关注编译器版本更新,并在支持的目标平台上启用相应优化选项。随着该技术逐步进入主流编译器发行版,预计将在更多应用场景中产生可观的性能收益。

资料来源:本文技术细节主要参考 arXiv 论文 "Optimization of 32-bit Unsigned Division by Constants on 64-bit Targets"(arXiv:2604.07902),该论文提出了针对 33 位魔法常数的单指令优化方法,并提供了 LLVM 实现的完整技术细节与基准测试数据。

compilers