[英]Can 128bit/64bit hardware unsigned division be faster in some cases than 64bit/32bit division on x86-64 Intel/AMD CPUs?
可以通过硬件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
...在某些特殊情况下,比硬件64位/ 32位除法指令执行的缩放64位/ 32位除法更快,例如:
; 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
指令感兴趣。
您正在问关于将uint64_t / uint64_t
C除法优化为64b / 32b => 32b x86 asm除法(已知除数为32位)的问题。 当然,编译器必须避免在完全有效的(在C语言中)64位除法中出现#DE
异常的可能性,否则,它就不会遵循as-if规则。 因此,只有在商数可以容纳32位的情况下,它才能执行此操作。
是的,那是一场胜利,或者至少是收支平衡。 在某些CPU上,甚至值得在运行时检查这种可能性,因为64位除法速度要慢得多。 但不幸的是当前的x86编译器不具有优化通寻找这个优化 ,即使你设法给他们足够的信息,他们可以证明它是安全的。 例如, if (edx >= ebx) __builtin_unreachable();
上次尝试没有帮助。
16或8位可能比32慢,因为它们可能会有错误的依赖性来写入输出,但是为了避免这种情况,写入32位寄存器零扩展到64。 (这就是mov ecx, ebx
是将ebx零扩展到64位的好方法的原因,比harhar所指出的要好, and
该值不能编码为32位符号扩展的立即数。) 但是,除了部分寄存器的恶作剧外,16位和8位除法运算速度通常也与32位一样快,甚至还不差。
在AMD CPU上,除法性能不取决于操作数大小,而仅取决于数据 。 128/64位的0 / 1
应该比任何较小的操作数大小的最坏情况都要快。 AMD的整数除法指令只有2微秒(大概是因为它必须写入2个寄存器),所有逻辑都在执行单元中完成。
Ryzen上的16位/ 8位=> 8位除法是单个uop(因为它只需要写AH:AL = AX)。
在Intel CPU上, div
/ idiv
被微编码为尽可能多的微码 。 对于最大32位(Skylake = 10)的所有操作数大小,大约相同的uops数量,但是64位要慢得多 。 (Skylake div r64
为36 div r64
,Skylake idiv r64
为57 idiv r64
)。 请参阅Agner Fog的说明表: https ://agner.org/optimize/
在Skylake上,最大32位操作数大小的div / idiv吞吐量固定为每6个周期1个。 但是div/idiv r64
吞吐量是每24-90个周期之一。
对于特定的性能实验,通过修改现有二进制文件中的REX.W前缀将div r64
更改为div r32
在Windows上,Trial-division代码在32位上的运行速度比Linux在64位上运行的快2倍。吞吐量差异。
为什么Clang仅从Sandy Bridge开始才做这种优化技巧? 显示了当英特尔CPU进行调整时,当股息较小时,机会性地使用32位除法的clang。 但是您有一个大红利和一个足够大的除数,这是一个更复杂的情况。 那种clang优化仍然使asm的上半部分清零,从不使用非零或非符号扩展的EDX。
当将一个无符号的32位整数(左移32位)除以另一个32位整数时,我未能使流行的C编译器生成后者的代码。
我假设你投的是32位整数uint64_t
第一 ,避免UB,并得到一个正常的uint64_t / uint64_t
在C抽象机。
这是有道理的: 您的方式将不安全,当edx >= ebx
时,它将以#DE
错误。 当商溢出AL / AX / EAX / RAX而不是默默截断时,x86除法会发生故障。 无法禁用它。
所以编译器通常只使用idiv
后cdq
或cqo
,和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将这些情况描述为“未定义的行为”。 一些ISA(如ARM)具有无故障的划分指令。 C UB表示可能发生任何事情,包括仅截断为0或其他整数结果。 请参见为什么将-1除以整数(负数)会导致FPE? 有关AArch64与x86代码生成和结果的示例。 允许故障并不意味着需要故障。
在某些情况下,在x86-64 Intel / AMD CPU上,128bit / 64bit硬件无符号除法能否比64bit / 32bit除法更快?
从理论上讲,一切皆有可能(例如,在50年后,Nvidia会创建一个80x86 CPU ...)。
但是,我想不出一个单一的合理原因,为什么在x86-64上128bit / 64bit的分割速度会比(不仅等同于)64bit / 32bit的分割速度更快。
我怀疑这是因为我假设C编译器作者非常聪明,并且到目前为止,当我将无符号的32位整数(左移32位)除以另一个32位整数时,我未能使流行的C编译器生成后一个代码。 。 它始终编译为128位/ 64位div指令。 PS左移编译为
shl
很好。
编译器开发人员很聪明,但是编译器很复杂,并且C语言规则妨碍了编译。 例如,如果您只是执行a a = b/c;
( b
为64位, c
为32位)时,该语言的规则是c
在除法发生之前被提升为64位,因此最终以某种中间语言成为64位除数,这使得后端翻译(从中间语言到汇编语言)很难说出64位除数可以是32位除数。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.