首页 > 解决方案 > 如何指示 MSVC 编译器使用 64 位/32 位除法而不是较慢的 128 位/64 位除法?

问题描述

如何告诉 MSVC 编译器使用 64 位/32 位除法运算来计算 x86-64 目标的以下函数的结果:

#include <stdint.h> 

uint32_t ScaledDiv(uint32_t a, uint32_t b) 
{
  if (a > b)
        return ((uint64_t)b<<32) / a;   //Yes, this must be casted because the result of b<<32 is undefined
  else
        return uint32_t(-1);
}

当语句为真时,我希望代码if编译为使用 64 位/32 位除法运算,例如:

; Assume arguments on entry are: Dividend in EDX, Divisor in ECX
mov edx, edx  ;A dummy instruction to indicate that the dividend is already where it is supposed to be
xor eax,eax
div ecx   ; EAX = EDX:EAX / ECX

...但是 x64 MSVC 编译器坚持使用 128bit/64bitdiv指令,例如:

mov     eax, edx
xor     edx, edx
shl     rax, 32                             ; Scale up the dividend
mov     ecx, ecx
div rcx   ;RAX = RDX:RAX / RCX

见:https ://www.godbolt.org/z/VBK4R7 1

根据this question的答案,128bit/64bitdiv指令并不比64bit/32bitdiv指令快。

这是一个问题,因为它不必要地减慢了我的 DSP 算法的速度,从而产生了数百万个这样的缩放分区。

我通过修补可执行文件以使用 64 位/32 位 div 指令来测试这种优化:根据指令产生的两个时间戳,性能提高了 28%rdtsc

(编者注:大概在最近的一些英特尔 CPU 上。AMD CPU 不需要这种微优化,如链接的问答中所述。)

标签: cvisual-c++x86-64compiler-optimizationinteger-division

解决方案


当前的编译器 (gcc/clang/ICC/MSVC) 不会从可移植的 ISO C 源代码中进行此优化,即使您让他们证明这一点,b < a所以商将适合 32 位。(例如if(b>=a) __builtin_unreachable(); 在 Godbolt 上使用 GNU C )。这是一个错过的优化;在修复之前,您必须使用内在函数或内联汇编来解决它。

(或者使用 GPU 或 SIMD 代替;如果您对许多元素使用相同的除数,请参阅https://libdivide.com/以便 SIMD 计算一次乘法逆并重复应用它。)


_udiv64从 Visual Studio 2019 RTM 开始可用。

在 C 模式 ( -TC) 中,它显然总是被定义的。在 C++ 模式下,您需要#include <immintrin.h>按照 Microsoft 文档。或intrin.h

https://godbolt.org/z/vVZ25L (或在 Godbolt.ms 上,因为最近在 Godbolt 主站点上的 MSVC 无法正常工作1。)

#include <stdint.h>
#include <immintrin.h>       // defines the prototype

// pre-condition: a > b else 64/32-bit division overflows
uint32_t ScaledDiv(uint32_t a, uint32_t b) 
{
    uint32_t remainder;
    uint64_t d = ((uint64_t) b) << 32;
    return _udiv64(d, a, &remainder);
}

int main() {
    uint32_t c = ScaledDiv(5, 4);
    return c;
}

_udiv64 将产生 64/32 div。左右两班是错过的优化。

;; MSVC 19.20 -O2 -TC
a$ = 8
b$ = 16
ScaledDiv PROC                                      ; COMDAT
        mov     edx, edx
        shl     rdx, 32                             ; 00000020H
        mov     rax, rdx
        shr     rdx, 32                             ; 00000020H
        div     ecx
        ret     0
ScaledDiv ENDP

main    PROC                                            ; COMDAT
        xor     eax, eax
        mov     edx, 4
        mov     ecx, 5
        div     ecx
        ret     0
main    ENDP

所以我们可以看到 MSVC 不会通过 进行常量传播_udiv64,即使在这种情况下它不会溢出并且它可能已经编译mainmov eax, 0ccccccccH/ ret


更新 #2 https://godbolt.org/z/n3Dyp- 使用英特尔 C++ 编译器添加了一个解决方案,但这效率较低,并且会破坏常量传播,因为它是内联汇编。

#include <stdio.h>
#include <stdint.h>

__declspec(regcall, naked) uint32_t ScaledDiv(uint32_t a, uint32_t b) 
{
    __asm mov edx, eax
    __asm xor eax, eax
    __asm div ecx
    __asm ret
    // implicit return of EAX is supported by MSVC, and hopefully ICC
    // even when inlining + optimizing
}

int main()
{
    uint32_t a = 3 , b = 4, c = ScaledDiv(a, b);
    printf( "(%u << 32) / %u = %u\n", a, b, c);
    uint32_t d = ((uint64_t)a << 32) / b;
    printf( "(%u << 32) / %u = %u\n", a, b, d);
    return c != d;
}

脚注 1:Matt Godbolt 的主站点的非 WINE MSVC 编译器暂时(?)消失了。Microsoft 运行https://www.godbolt.ms/以在真实 Windows 上托管最近的 MSVC 编译器,并且通常主要的 Godbolt.org 站点中继到 MSVC。)

似乎 godbolt.ms 会生成短链接,但不会再次扩展它们!无论如何,完整的链接更好地抵抗链接腐烂。


推荐阅读