当除数已知为 32 位时,您问的是优化 uint64_t / uint64_t C 除法为 64b / 32b => 32b x86 asm 除法。编译器当然必须避免在完全有效的(在 C 中)64 位除法上出现#DE 异常的可能性,否则它不会遵循 as-if 规则。所以它只有在可以证明商适合 32 位时才能这样做。
是的,这是一场胜利,或者至少是收支平衡。在某些 CPU 上,甚至值得在运行时检查这种可能性,因为 64 位除法要慢得多。 但不幸的是,当前的 x86 编译器没有优化器通道来寻找这种优化,即使您确实设法向他们提供了足够的信息,他们可以证明它是安全的。例如if (edx >= ebx) __builtin_unreachable(); 上次我尝试没有帮助。
对于相同的输入,32 位操作数大小总是至少一样快
16 位或 8 位可能比 32 慢,因为它们在写入输出时可能存在错误的依赖关系,但写入 32 位寄存器零扩展至 64 以避免这种情况。 (这就是为什么mov ecx, ebx 是将 ebx 零扩展为 64 位的好方法,优于 and 一个不可编码为 32 位符号扩展立即数的值,就像 harold 指出的那样)。但除了部分寄存器的恶作剧之外,16 位和 8 位除法通常也与 32 位一样快,或者不会更差。
在 AMD CPU 上,除法性能不取决于操作数大小,而仅取决于数据。 128/64 位的0 / 1 应该比任何较小操作数大小的最坏情况更快。 AMD的整数除法指令只有2个微指令(大概是因为它要写2个寄存器),所有的逻辑都在执行单元中完成。
16 位 / 8 位 => Ryzen 上的 8 位除法是单个 uop(因为它只需要写 AH:AL = AX)。
在 Intel CPU 上,div/idiv 被微编码为许多微指令。对于最大为 32 位 (Skylake = 10) 的所有操作数大小,微指令的数量大致相同,但 64 位要慢得多慢得多。 (Skylake div r64 是 36 微指令,Skylake idiv r64 是 57 微指令)。参见 Agner Fog 的指令表:https://agner.org/optimize/
在 Skylake 上,操作数大小高达 32 位的 div/idiv 吞吐量固定为每 6 个周期 1 个。但是div/idiv r64 的吞吐量是每 24-90 个周期一个。
另请参阅 Trial-division code runs 2x faster as 32-bit on Windows than 64-bit on Linux 以了解特定性能实验,其中修改现有二进制文件中的 REX.W 前缀以将 div r64 更改为 div r32 会导致吞吐量差异约 3 倍。
而Why does Clang do this optimization trick only from Sandy Bridge onward? 显示在调整英特尔 CPU 时,当红利较小时,clang 机会主义地使用 32 位除法。但是你有一个很大的股息和一个足够大的除数,这是一个更复杂的情况。该 clang 优化仍然将 asm 中除数的上半部分归零,从不使用非零或非符号扩展的 EDX。
在将一个无符号 32 位整数(左移 32 位)除以另一个 32 位整数时,我未能让流行的 C 编译器生成后一个代码。
我假设您将 32 位整数转换为 uint64_tfirst,以避免 UB 并在 C 抽象机中获得正常的 uint64_t / uint64_t。
这是有道理的:你的方式不安全,edx >= ebx 时会出现#DE 错误。 当商溢出 AL / AX / EAX / RAX 时,x86 除法错误,而不是默默地截断。没有办法禁用它。
所以编译器通常只在cdq 或cqo 之后使用idiv,而div 只在将高半部分归零之后使用,除非您使用内部或内联汇编来让自己接受代码错误的可能性.在 C 中,x / y 仅在 y = 0 时出错(或者对于签名,INT_MIN / -1 也允许出错1)。
GNU C 没有宽除的内在函数,但 MSVC 有 _udiv64。 (对于 gcc/clang,比 1 个寄存器更宽的除法使用一个帮助函数,它会尝试优化小输入。但这对于 64 位机器上的 64/32 除法没有帮助,其中 GCC 和 clang 只使用 128 /64 位除法指令。)
即使有某种方法可以向编译器保证您的除数足够大以使商适合 32 位,但根据我的经验,当前的 gcc 和 clang 并不寻求这种优化。对于您的情况,这将是一个有用的优化(如果它总是安全的话),但编译器不会寻找它。
脚注 1:更具体地说,ISO C 将这些情况描述为“未定义的行为”;一些像 ARM 的 ISA 有无故障除法指令。 C UB 表示任何事情都可能发生,包括截断为 0 或其他整数结果。有关 AArch64 与 x86 代码生成和结果的示例,请参阅 Why does integer division by -1 (negative one) result in FPE?。 允许出错并不意味着需要出错。