如何告诉MSVC编译器使用64位/ 32位除法而不是较慢的128位/ 64位除法?

时间:2019-06-18 21:22:41

标签: c visual-c++ 64-bit x86-64 integer-division

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

UINT32 ScaledDiv(UINT32 a, UIN32 b)  // Always a > b
{
  return ((UINT64)b<<32) / a;   //Yes, this must be casted because the result of b<<32 is undefined
}

我希望将此函数编译为使用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 / 64bit div指令,例如:

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.ms/z/I2qFSk

根据this question的答案,128位/ 64位div指令比64位/ 32位div指令不快

这是一个问题,因为它不必要地减慢了我的DSP算法的速度,而DSP算法却使成千上万的这种比例划分成为可能。

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

(编者注:大概是在某些最新的Intel CPU上。AMDCPU不需要这种微优化,如链接的问答中所述。)

2 个答案:

答案 0 :(得分:6)

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

(或改为使用GPU或SIMD;如果许多元素具有相同的除数,请参见https://libdivide.com/,让SIMD计算一次乘法逆并重复应用。)


_udiv64 is available从Visual Studio 2019 RTM开始。

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

https://godbolt.org/z/vVZ25L(或on 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进行恒定传播,即使在这种情况下它也不会溢出并且可以将main编译为mov eax, 0ccccccccH / ret


更新#2 https://godbolt.org/z/n3Dyp- 添加了使用Intel C ++编译器的解决方案,但是效率较低,并且由于它是嵌入式asm,因此将无法进行恒定传播。

#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编译器暂时消失了?微软运行https://www.godbolt.ms/在真实Windows上托管最新的MSVC编译器,并且通常将Godbolt.org主站点中继到MSVC的主站点。)

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

答案 1 :(得分:4)

@Alex Lopatin的答案显示了如何使用_udiv64获取不可怕的标量代码(尽管MSVC的愚蠢的优化错过了向左/向右移动)。

对于支持GNU C内联汇编(包括ICC)的编译器,您可以使用它来代替效率低下的MSVC内联汇编语法,该语法对于包装一条指令有很多开销。有关包装64位/ 32位=> 32位idiv的示例,请参见What is the difference between 'asm', '__asm' and '__asm__'?。 (仅通过将助记符和类型更改为unsigned即可将其用于div。)GNU C没有用于64/32或128/64除法的内在函数;可以优化纯C语言。但是不幸的是,即使使用if(a<=b) __builtin_unreachable();来保证a>b,GCC / Clang / ICC都没有为此情况优化。


但这仍然是标量除法,吞吐量很差。

也许您可以为您的DSP任务使用GPU?如果您有足够的工作量(其余算法对GPU友好),那么与GPU进行通信往返的开销可能就值得。

如果您使用的是CPU,那么我们建议采取的任何措施都会受益于并行化多个内核,因此这样做可以提高吞吐量。


x86 SIMD(SSE4 / AVX2 / AVX512 *)在硬件中没有SIMD整数除法。英特尔SVML函数_mm_div_epu64 and _mm256_div_epu64不是真正的指令的内在函数,它们是缓慢的函数,可能解压缩成标量或计算乘法逆。或他们使用的其他任何技巧;可能是32位除法函数转换为double的SIMD向量,尤其是在AVX512可用的情况下。 (英特尔之所以仍称其为“本征”,可能是因为它们就像是它可以理解的内置函数并且可以通过它进行恒定传播。它们可能会尽可能地高效,但是那不是“非常”,他们需要来处理一般情况,而不仅仅是特殊情况,一个除数的下半部分全为零,并且商数拟合为32位。)

如果许多元素具有相同的除数,请参见https://libdivide.com/,以使SIMD一次计算一个乘法逆,然后重复应用它。 (您应该修改该技术以在不实际进行分红的情况下进行烘烤,而将全零的低半部分保留为隐式。)

如果除数总是变化的,并且这不是某些较大的SIMD友好算法的中间步骤,则标量除法可能是您最好的选择,如果您需要精确的结果。


如果24位尾数精度足够,则可以通过使用SIMD float来大大提高速度

uint32_t ScaledDiv(uint32_t a, uint32_t b) 
{
    return ((1ULL<<32) * (float)b) / a;
}

(float)(1ULL<<32)是一个编译时常量4294967296.0f

这确实可以在数组上自动矢量化,即使没有-ffast-math,也可以使用gcc和clang(但不包括MSVC)。 See it on Godbolt。您可以将gcc或clang的asm移植回MSVC的内在函数。他们使用一些FP技巧将无符号整数压缩打包转换为没有AVX512的float。非矢量标量FP可能会比MSVC上的纯整数慢,并且准确性较低。

例如,Skylake的div r32吞吐量是每6个周期1个。但是其AVX vdivps ymm吞吐量是每5个周期1条指令(共8 float s条)。或对于128位SSE2,divps xmm每3个周期就有一个吞吐量。 因此,您从Skylake上的AVX获得大约10倍的分区吞吐量。 (8 * 6/5 = 9.6)旧的微体系结构的SIMD FP划分要慢得多,而整数划分也要慢一些。通常,此比率较小,因为较旧的CPU没有较宽的SIMD分频器,因此256位vdivps必须分别运行128位半部分。但是仍然有很多收获,就像比Haswell的四分之一更好。并且Ryzen的vdivps ymm吞吐量为6c,但是div 32吞吐量为14-30个周期。因此,这比Skylake还要大。

如果您的其余DSP任务可以从SIMD中受益,则总体速度应该会非常好。 float操作具有较高的延迟,因此乱序执行必须更努力地工作以隐藏该延迟,并使独立循环迭代的执行重叠。因此, IDK是对您来说,将其转换为浮点数并返回该操作更好,还是将算法更改为在float处均可使用会更好。这取决于您还需要处理数字。


如果您的无符号数字实际上适合 signed 32位整数,则可以使用直接硬件支持来打包SIMD int32->浮点转换。否则,您需要使用单个指令来将AVX512F用于打包的uint32-> float,但是可以通过效率降低来模拟它。这就是gcc / clang在使用AVX2自动矢量化时所做的事情,以及为什么MSVC 自动矢量化的原因。

MSVC确实使用int32_t而不是uint32_t自动向量化(并且gcc / clang可以使代码更有效),因此,如果整数输入和/或输出的最高位可以待定。 (即其位模式的2的补码解释为非负数。)

特别是对于AVX,vdivps的速度足够慢,几乎可以掩盖从整数到整数的转换的吞吐量成本,除非还有其他有用的工作可能重叠了。


浮点精度:

float将数字存储为significand * 2^exp,其中有效数在[1.0, 2.0)范围内。 (或[0, 1.0)表示次法线)。单精度float具有24位有效精度,其中包括1个隐含位。

https://en.wikipedia.org/wiki/Single-precision_floating-point_format

因此可以表示整数的24个最高有效位,其余的则舍入到舍入误差。像(uint64_t)b << 32这样的整数对float来说没问题;那只是意味着更大的指数。低位全为零。

例如,b = 123105810528735427897589760给了我们b64 << 32。将其直接从64位整数转换为float会得到528735419307655168,舍入误差为0.0000016%,或大约2 ^ -25.8。这不足为奇:最大舍入误差为0.5ulp(最后一位的单位)或2 ^ -25,并且该数字是偶数,因此无论如何它都有1个尾随零。这与转换123105810时会得到的相对错误相同;所得的float除其指数字段(高32)外也相同。

(我使用https://www.h-schmidt.net/FloatConverter/IEEE754.html进行了检查。)

float的最大指数足以容纳INT64_MININT64_MAX范围之外的整数。 float可以表示的大整数的低位全为零,但这正是b<<32所具有的。因此,在最坏的情况下,b的低9位只有全范围和奇数。

如果结果的重要部分是最高有效位,并且低至〜9的整数位=转换回整数后可以舍入误差,那么float对您来说是完美的。

如果float不起作用,则可以选择double

在许多CPU上,

divpd的速度大约是divps的两倍,并且仅完成一半的工作(2个double元素而不是4个float)。这样您将损失4倍的吞吐量。

但是每个32位整数都可以精确地表示为double而且,通过将截断值返回零,我认为您可以对所有输入对进行精确的整数除法,除非double-rounding is a problem (first to nearest double, then truncation)。您可以使用

进行测试
// exactly correct for most inputs at least, maybe all.
uint32_t quotient = ((1ULL<<32) * (double)b) / a;

无符号长整型常量(1ULL<<32)被转换为double,因此您进行了2次u32->双转换(ab),双倍乘法,两次除法,然后两次-> u32转换。 x86-64可以通过标量转换有效地完成所有这些操作(通过将uint32_t零扩展为int64_t,或者忽略double-> int64_t转换的高位),但它可能仍然比div r32

转换u32->双向并返回(不带AVX512)可能比转换u32-> float更昂贵,但是clang 可以自动对其向量化。  (只需在上方的Godbolt链接中将float更改为double)。同样,如果您的输入全部为<= INT32_MAX,将会很有帮助,因此可以将它们视为FP转换的有符号整数。

如果双舍入是一个问题,则可以将FP舍入模式设置为截断,而不是默认的舍入到最近舍入,如果在运行DSP代码的线程中不对其他任何函数使用FP