如何让 GCC 编译器将变量划分转换为 mul(如果更快)

How to let GCC compiler turn variable-division into mul(if faster)

本文关键字:mul 如果 转换 划分 GCC 编译器 变量      更新时间:2023-10-16
int a, b;
scanf("%d %d", &a, &b);
printf("%dn", (unsigned int)a/(unsigned char)b);

编译时,我得到了 ...

    ::00401C1E::  C70424 24304000          MOV DWORD PTR [ESP],403024  %d %d
    ::00401C25::  E8 36FFFFFF              CALL 00401B60               scanf
    ::00401C2A::  0FB64C24 1C              MOVZX ECX,BYTE PTR [ESP+1C]
    ::00401C2F::  8B4424 18                MOV EAX,[ESP+18]                        
    ::00401C33::  31D2                     XOR EDX,EDX                             
    ::00401C35::  F7F1                     DIV ECX                                 
    ::00401C37::  894424 04                MOV [ESP+4],EAX                         
    ::00401C3B::  C70424 2A304000          MOV DWORD PTR [ESP],40302A  %dx0A
    ::00401C42::  E8 21FFFFFF              CALL 00401B68               printf

如果 DIV 变成 MUL 并使用数组来存储 mulvalue 会更快吗?如果是这样,如何让编译器进行优化?

int main() {
    uint a, s=0, i, t;
    scanf("%d", &a);
    diviuint aa = a;
    t = clock();
    for (i=0; i<1000000000; i++)
        s += i/a;
    printf("Result:%10un", s);
    printf("Time:%12un", clock()-t);
    return 0;
}

其中diviuint(a) 使内存为 1/A 并使用多个来代替使用 s+=i/aa 使速度是 s+=i/a<</p>

div class="answers" 的 2 倍>

您是对的,如果在循环中不可避免地进行整数除法,那么找到乘法逆可能是值得的。 不过,GCC 和 Clang 不会使用运行时常量为您执行此操作;仅编译时常量。 对于编译器来说,在不确定是否需要的情况下,它的成本太高(代码大小),并且非编译时常量的性能增益没有那么大。 (我不相信加速总是可能的,这取决于目标微架构上的整数除法有多好。


使用乘法逆

如果您无法转换事物以将分界线从循环中拉出,并且它运行多次迭代,并且代码大小的显着增加是性能的提高(例如,您不会因隐藏div 延迟的缓存未命中而受到瓶颈),那么您可能会从对运行时常量执行编译器对编译时常量所做的操作中获得加速。

请注意,不同的常量需要

全乘法高半部分的不同移位,并且某些常量需要比其他常量更多不同的移位。 (另一种说法是某些常量的某些移位计数为零)。 因此,非编译时间常数除以乘法代码需要所有移位,并且移位计数必须是变量计数。 (在 x86 上,这比即时计数班次更昂贵)。

libdivide实现了必要的数学运算。 我认为,您可以使用它来进行 SIMD 矢量化除法,或者用于标量。 这肯定会比解包到标量并在那里进行整数除法提供很大的加速。 我自己没有用过。

(英特尔 SSE/AVX 在硬件中不执行整数除法,但提供了多种乘法和相当高效的可变计数移位指令。 对于 16 位元素,有一条指令只产生乘法的高半部分。 对于 32 位元素,有一个加宽的乘法,所以你需要一个随机的。

无论如何,您可以使用 libdivide 对该添加循环进行矢量化,最后有一个水平总和。


让div 退出循环的其他方法

for (i=0; i<1000000000; i++)
    s += i/a;

在您的示例中,通过使用uint128_t s累加器并除以循环外的a,您可能会获得更好的结果。 64位添加/ADC对非常便宜。 (但是,它不会给出相同的结果,因为整数除法会截断而不是舍入到最接近。

我认为你可以通过循环使用 i += a; tmp++ 来解释这一点,并执行s += tmp*a,以组合来自i/a相同迭代的所有添加。 因此,s += 1 * a考虑了i = [a .. a*2-1]的所有迭代。 显然,这只是一个微不足道的例子,通常不可能更有效地循环。 这个问题是题外话,但无论如何都值得一提:在尝试更快地完成完全相同的事情之前,通过重新构建代码或利用一些数学来寻找大的优化。 说到数学,你可以在这里使用sum(0..n) = n * (n+1) / 2公式,因为我们可以从a*1 + a*2 + a*3 ... a*maxa因素。 我在这里可能有一个 off-by-one,但我相信封闭形式的简单常量时间计算将给出与任何a循环相同的答案:

uint32_t n = 1000000000 / a;
uint32_t s = a * n*(n+1)/2 + 1000000000 % a;

如果您只需要循环中的i/a,那么

执行以下操作可能是值得的:
// another optimization for an unlikely case
for (uint32_t i=0, remainder=0, i_over_a=0 ; i < n ; i++) {
    // use i_over_a
    ++remainder;
    if (remainder == a) {        // if you don't need the remainder in the loop, it could save an insn or two to count down from a to 0 instead of up from 0 to a, e.g. on x86.  But then you need a clever variable name other than remainder.
        remainder = 0;
        ++i_over_a;
    }
}

同样,这不太可能:它仅在将循环计数器除以常量时才有效。 但是,它应该运行良好。 要么a很大,所以分支错误预测将不常见,要么a(希望)足够小,以便一个好的分支预测器以一种方式识别a-1分支的重复模式,然后以另一种方式识别 1 个分支。 最坏情况下的a值可能是 33 或 65 或更高,具体取决于微体系结构。 无分支asm可能是可能的,但不值得。 例如,使用附加进位和有条件的归零移动来处理++i_over_a。(例如 x86 伪代码cmp a-1, remainder/cmovc remainder, 0/adc i_over_a, 0b(下面)条件刚好CF==1,与c(携带)条件相同。 无分支 asm 将通过从 a 递减到 0 来简化。 (CMOV不需要归零的注册,并且可以在注册表中a而不是a-1))

当其中一个值在编译时已知时,将 DIV 替换为 MUL 可能是有意义的(但并非在所有情况下都必须如此)。当两者都是用户输入时,您不知道范围是多少,因此所有常用技巧都不起作用。

基本上,您需要处理INT_MAXINT_MIN之间的ab。没有空间可以放大/缩小它们。即使要将它们扩展到更大的类型,也可能需要更长的时间才能反转b并检查结果是否一致。

知道divmul是否更快的唯一方法是在基准测试中测试两者[显然,如果您使用上面的代码,您将主要测量输入和结果的读/写时间,而不是实际的除法指令,因此您需要一些可以将除法指令与输入和输出隔离开来的东西]。

我的猜测是,在稍旧的处理器上,mul会快一些,在现代处理器上,div将与查找 256 个int值的速度一样快,如果不是更快的话。

如果您有一个目标系统,那么测试它是合理的。如果你有几个不同的系统想要运行,你必须确保"改进的代码"至少在其中一些系统上更快 - 而不是在其余系统上明显变慢。

另请注意,您将引入依赖关系,这本身可能会减慢操作序列 - 只要有其他指令要执行,现代 CPU 就非常擅长"隐藏"延迟 [所以你应该在"尽可能现实的场景中"使用它]。

这个问题有一个错误的假设。大于 1 的整数的乘法逆是小于 1 的分数。这些在整数世界中不存在。查找表不起作用,因为您无法查找不存在的内容。即使你"缩放"股息,结果在与整数除法相同的意义上也不会正确。举个例子:

printf("%x %xn", 0x10/0x9, 0x30/0x9);
// prints: 1 5

假设存在乘法逆,则两个项被相同的除数 (9) 除以,因此必须具有相同的查找表值(乘法逆)。对应于除数 (9) 乘以整数的任何固定查找值在第二项中将恰好是相对于第一项的 3 倍。从示例中可以看出,实际整数除法的结果是 5,而不是 3。

您可以使用缩放查找表来估算事物。例如,当结果除以 2^16 时,查找表是乘法逆。然后,您将乘以查找表值并将结果向右移动 16 位。耗时且需要 1024 字节的查找表。即便如此,这也不会产生与整数除法相同的结果。编译器优化不会产生整数除法的"近似"结果。

相关文章: