繁体   English   中英

当被除数为 64 位且商为 32 位时,如何使 gcc 或 clang 使用 64 位/32 位除法而不是 128 位/64 位除法?

[英]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_cdiv_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 r32div 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表示从ad寄存器( eaxedx )读取和写入值。 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM