可以通过硬件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
指令。
答案 0 :(得分:5)
您要询问的问题是:将uint64_t / uint64_t
C除法优化为64b / 32b => 32b x86 asm除法(已知除数为32位)。当然,编译器必须避免在完全有效的(在C中)64位除法中发生#DE
异常的可能性,否则,它就不会遵循as-if规则。因此,只有在商数可以容纳32位的情况下,才能执行此操作。
是的,这是胜利或至少是收支平衡。在某些CPU上,甚至值得在运行时检查这种可能性,因为64位除法速度要慢得多。 但是不幸的是,即使您设法向x86编译器提供足够的信息以使其可以证明自己是安全的,即使当前x86编译器也没有通过优化程序来寻找这种优化方式。例如if (edx >= ebx) __builtin_unreachable();
对我上次尝试没有帮助。
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)的所有操作数,大约有相同的oups数量,但是 64位要慢得多 。 (Skylake div r64
是36 uops,Skylake idiv r64
是57 uops)。请参阅Agner Fog的说明表:https://agner.org/optimize/
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?表示在调整Intel CPU时,当股息较小时,机会地使用32位除法。但是您有一个大红利和一个足够大的除数,这是一个更复杂的情况。这种clang优化仍然使asm的上半部分清零,从不使用非零或非符号扩展的EDX。
当将一个无符号的32位整数(左移32位)除以另一个32位整数时,我未能使流行的C编译器生成后一个代码。
我假设您将32位整数强制转换为uint64_t
first ,以避免UB并在C抽象机中获得正常的uint64_t / uint64_t
。
这是有道理的:您的方式并不安全,当#DE
时,它将以edx >= ebx
出现故障。商因AL / AX / EAX溢出而x86除法出错/ RAX,而不是默默地截断。无法禁用它。
因此,编译器通常仅在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或其他整数结果。有关AArch64与x86代码生成和结果的示例,请参见Why does integer division by -1 (negative one) result in FPE?。 允许表示错误并不表示需要表示错误。
答案 1 :(得分:2)
在某些情况下,在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指令。附言左移可以编译为
shl
。
编译器开发人员很聪明,但是编译器很复杂,并且C语言规则妨碍了编译。例如,如果您只是进行a = b/c;
(b
为64位而c
为32位),则该语言的规则是c
被提升为64位在除法发生之前,它最终成为某种中间语言的64位除数,这使得后端翻译(从中间语言到汇编语言)很难说出64位除数可以成为32位除数。