【问题标题】:Can 128bit/64bit hardware unsigned division be faster in some cases than 64bit/32bit division on x86-64 Intel/AMD CPUs?在某些情况下,128 位/64 位硬件无符号除法是否比 x86-64 Intel/AMD CPU 上的 64 位/32 位除法更快?
【发布时间】:2019-11-01 11:14:21
【问题描述】:

可以通过硬件128bit/64bit除法指令进行缩放的64bit/32bit除法,如:

; Entry arguments: Dividend in EAX, Divisor in EBX
shl rax, 32  ;Scale up the Dividend by 2^32
xor rdx,rdx
and rbx, 0xFFFFFFFF  ;Clear any garbage that might have been in the upper half of RBX
div rbx  ; RAX = RDX:RAX / RBX

...在某些特殊情况下比硬件 64bit/32bit 除法指令执行的缩放的 64bit/32bit 除法更快,例如:

; Entry arguments: Dividend in EAX, Divisor in EBX
mov edx,eax  ;Scale up the Dividend by 2^32
xor eax,eax
div ebx  ; EAX = EDX:EAX / EBX

“一些特殊情况”是指不寻常的红利和除数。 我只对比较div 指令感兴趣。

【问题讨论】:

  • 这是个好问题。是什么让你怀疑是这种情况?
  • 我怀疑这是因为我认为 C 编译器的作者非常聪明,到目前为止,我未能让流行的 C 编译器在除以无符号 32 位整数(左移 32位)由另一个 32 位整数。它总是编译成 128bit/64bit div 指令。附言左移编译为shl
  • 顺便说一句,而不是and rbx, 0xFFFFFFFF(这是不可能的,但我明白你的意思)你可以写mov ebx, ebx
  • @harold:真的吗?! mov ebx, ebx 会清除 rbx 的高 32 位?
  • @GeorgeRobinson:每条修改 32 位寄存器的指令(例如 ebx)都会导致 64 位寄存器的高 32 位归零。请注意(因此)ebx 中较高的 32 位可能已经从早期(未显示)指令中归零。

标签: performance assembly x86 x86-64 integer-division


【解决方案1】:

当除数已知为 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 除法错误,而不是默默地截断。没有办法禁用它。

所以编译器通常只在cdqcqo 之后使用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?允许出错并不意味着需要出错。

【讨论】:

  • 我认为您的意思是“mov ecx, ebx 是将 ecx 零扩展为 64 位的好方法,”...您写道“...将 ebx 零扩展为64 位,
  • 我知道div ebx 会在ebx<=edx 时溢出并出现故障,但我可以接受这种故障,因为它表明我的函数的输入参数(除数和除数)是错误的.在我的函数中,除数总是大于被除数,我希望 C 编译器在选择 div 指令的大小时能够考虑到这一点。
  • @GeorgeRobinson:我的意思是“一种将 EBX 中的值零扩展为 64 位寄存器的好方法”。我选择了一个不同的目的地 (RCX),这样 mov-elimination 可以工作。 Can x86's MOV really be "free"? Why can't I reproduce this at all?
  • @GeorgeRobinson:如果您使用了if(b <= d) __builtin_unreachable();,那么您会发现 gcc 缺少优化:当 C 操作数为64 位。您需要使用内联 asm,因为我认为 gcc 没有用于 64/32 位 can-fault-on-overflow 划分的内置函数。
  • @PeterCordes 之类的,他们 OP 发布了一个 follow up question 我的评论基本上是为那些使用来自那里的答案的人提供一些背景信息。
【解决方案2】:

在某些情况下,128 位/64 位硬件无符号除法是否比 x86-64 Intel/AMD CPU 上的 64 位/32 位除法更快?

理论上,一切皆有可能(例如,Nvidia 可能会在 50 年后创建一个 80x86 CPU ......)。

但是,我想不出一个合理的理由为什么 128 位/64 位除法会比(不仅等同于)x86-64 上的 64 位/32 位除法更快。

我怀疑这是因为我假设 C 编译器的作者非常聪明,到目前为止,当将无符号 32 位整数(左移 32 位)除以另一个 32 时,我未能让流行的 C 编译器生成后一个代码位整数。它总是编译成 128bit/64bit div 指令。附言左移编译为shl

编译器开发人员很聪明,但编译器很复杂,而且 C 语言规则会妨碍您。例如,如果您只执行 a = b/c;b 为 64 位,c 为 32 位),则语言的规则是 c 在除法发生之前被提升为 64 位,因此它结束在某种中间语言中成为 64 位除数,这使得后端翻译(从中间语言到汇编语言)很难判断 64 位除数可能是 32 位除数。

【讨论】:

    猜你喜欢
    • 2010-12-24
    • 2021-06-03
    • 2018-07-18
    • 2011-03-29
    • 1970-01-01
    • 2019-11-01
    • 1970-01-01
    • 2015-05-02
    • 1970-01-01
    相关资源
    最近更新 更多