[英]How to make gcc or clang use 64-bit/32-bit division instead of 128-bit/64-bit division when the dividend is 64-bit and the quotient is 32-bit?
目前,通过研究和各种尝试,我很确定解决这个问题的唯一方法是使用汇编。 我发布这个问题是为了显示一个现有的问题,并且可能会引起编译器开发人员的注意,或者从有关类似问题的搜索中获得一些点击。
如果将来有任何变化,我会接受它作为答案。
这是一个与 MSVC 非常相关的问题。
在x86_64
机器中,使用 32 位操作数的div
/ idiv
比使用 64 位操作数更快。 当被除数为 64 位且除数为 32 位时,当您知道商适合 32 位时,您不必使用 64 位div
/ idiv
。 您可以将 64 位被除数拆分为两个 32 位寄存器,即使有这种开销,在两个 32 位寄存器上执行 32 位div
也会比使用完整的 64 位寄存器执行 64 位div
更快.
编译器将使用此 function 生成一个 64 位div
,这是正确的,因为对于 32 位div
,如果除法的商不适合 32 位,则会发生硬件异常。
uint32_t div_c(uint64_t a, uint32_t b) {
return a / b;
}
但是,如果已知商适合 32 位,则不需要进行完整的 64 位除法。 我用__builtin_unreachable
告诉编译器这个信息,但它没有任何区别。
uint32_t div_c_ur(uint64_t a, uint32_t b) {
uint64_t q = a / b;
if (q >= 1ull << 32) __builtin_unreachable();
return q;
}
对于div_c
和div_c_ur
,来自 gcc 的gcc
是,
mov rax, rdi
mov esi, esi
xor edx, edx
div rsi
ret
clang
对检查被除数大小进行了有趣的优化,但当被除数为 64 位时,它仍然使用 64 位div
。
mov rax, rdi
mov ecx, esi
mov rdx, rdi
shr rdx, 32
je .LBB0_1
xor edx, edx
div rcx
ret
.LBB0_1:
xor edx, edx
div ecx
ret
我必须直接在汇编中编写才能实现我想要的。 我找不到任何其他方法来做到这一点。
__attribute__((naked, sysv_abi))
uint32_t div_asm(uint64_t, uint32_t) {__asm__(
"mov eax, edi\n\t"
"mov rdx, rdi\n\t"
"shr rdx, 32\n\t"
"div esi\n\t"
"ret\n\t"
);}
它值得吗? 至少perf
报告div_asm
的开销为49.47%
,而div_c
的开销为24.88%
,因此在我的计算机(Tiger Lake)上, div r32
比div r64
快大约 2 倍。
这是基准代码。
#include <stdint.h>
#include <stdio.h>
__attribute__((noinline))
uint32_t div_c(uint64_t a, uint32_t b) {
uint64_t q = a / b;
if (q >= 1ull << 32) __builtin_unreachable();
return q;
}
__attribute__((noinline, naked, sysv_abi))
uint32_t div_asm(uint64_t, uint32_t) {__asm__(
"mov eax, edi\n\t"
"mov rdx, rdi\n\t"
"shr rdx, 32\n\t"
"div esi\n\t"
"ret\n\t"
);}
static uint64_t rdtscp() {
uint32_t _;
return __builtin_ia32_rdtscp(&_);
}
int main() {
#define n 500000000ll
uint64_t c;
c = rdtscp();
for (int i = 1; i <= n; ++i) {
volatile uint32_t _ = div_c(i + n * n, i + n);
}
printf(" c%15ul\n", rdtscp() - c);
c = rdtscp();
for (int i = 1; i <= n; ++i) {
volatile uint32_t _ = div_asm(i + n * n, i + n);
}
printf("asm%15ul\n", rdtscp() - c);
}
这个答案中的每个想法都是基于 Nate Eldredge 的评论,我从中发现了gcc
的扩展内联汇编的一些强大功能。 即使我仍然需要编写程序集,也可以创建一个自定义的仿佛内在 function。
static inline uint32_t divqd(uint64_t a, uint32_t b) {
if (__builtin_constant_p(b)) {
return a / b;
}
uint32_t lo = a;
uint32_t hi = a >> 32;
__asm__("div %2" : "+a" (lo), "+d" (hi) : "rm" (b));
return lo;
}
如果b
可以在编译时计算, __builtin_constant_p
返回1
。 +a
和+d
表示从a
和d
寄存器( eax
和edx
)读取和写入值。 rm
指定输入b
可以是寄存器或 memory 操作数。
要查看内联和常量传播是否顺利完成,
uint32_t divqd_r(uint64_t a, uint32_t b) {
return divqd(a, b);
}
divqd_r:
mov rdx, rdi
mov rax, rdi
shr rdx, 32
div esi
ret
uint32_t divqd_m(uint64_t a) {
extern uint32_t b;
return divqd(a, b);
}
divqd_m:
mov rdx, rdi
mov rax, rdi
shr rdx, 32
div DWORD PTR b[rip]
ret
uint32_t divqd_c(uint64_t a) {
return divqd(a, 12345);
}
divqd_c:
movabs rdx, 6120523590596543007
mov rax, rdi
mul rdx
shr rdx, 12
mov eax, edx
ret
结果令人满意( https://godbolt.org/z/47PE4ovMM )。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.