无论结果如何,支持除以零的最快整数除法是啥?

Posted

技术标签:

【中文标题】无论结果如何,支持除以零的最快整数除法是啥?【英文标题】:What is the fastest integer division supporting division by zero no matter what the result is?无论结果如何,支持除以零的最快整数除法是什么? 【发布时间】:2013-05-22 13:30:33 【问题描述】:

总结:

我正在寻找最快的计算方法

(int) x / (int) y

y==0 没有例外。相反,我只想要一个任意的结果。


背景:

在编码图像处理算法时,我经常需要除以(累积的)alpha 值。最简单的变体是带有整数运算的纯 C 代码。我的问题是,对于alpha==0 的结果像素,我通常会得到除以零的错误。然而,这正是结果根本不重要的像素:我不关心 alpha==0 的像素的颜色值。


详情:

我正在寻找类似的东西:

result = (y==0)? 0 : x/y;

result = x / MAX( y, 1 );

x 和 y 是正整数。代码在嵌套循环中执行了很多次,所以我正在寻找一种方法来摆脱条件分支。

当 y 不超过字节范围时,我对解决方案感到满意

unsigned char kill_zero_table[256] =  1, 1, 2, 3, 4, 5, 6, 7, [...] 255 ;
[...]
result = x / kill_zero_table[y];

但这显然不适用于更大的范围。

我想最后一个问题是:什么是最快的位旋转黑客将 0 更改为任何其他整数值,同时保持所有其他值不变?


澄清

我不是 100% 确定分支太昂贵。但是,由于使用了不同的编译器,所以我更喜欢几乎没有优化的基准测试(这确实值得怀疑)。

当然,编译器在位旋转方面非常出色,但我无法在 C 中表达“不关心”的结果,因此编译器将永远无法使用全部优化。

代码应与 C 完全兼容,主要平台是带有 gcc 和 clang 的 Linux 64 位以及 MacOS。

【问题讨论】:

你是如何确定 if-branch 太贵的? 你是如何确定一个分支的? +1 用于分析,使用现代分支预测,您可能不需要它。另外,为什么您要编写自己的图像处理算法? “什么是最快的比特旋转黑客......”也许y += !y?不需要分支来计算它。您可以将x / (y + !y)x / max(y, 1) 进行比较,也可以将y ? (x/y) : 0 进行比较。我猜它们中的任何一个都不会有分支,至少在启用优化的情况下。 任何认为现代分支预测意味着您不必这样做的人都没有分析足够多的在每个像素级别运行的分支消除代码。如果 alpha 0 部分很大且连续,则可以接受现代分支预测。有一个地方可以摆弄微优化,而每像素操作正是那个地方。 【参考方案1】:

受到一些 cmets 的启发,我摆脱了 Pentium 上的分支,并使用 gcc 编译器使用

int f (int x, int y)

        y += y == 0;
        return x/y;

编译器基本承认可以在加法中使用测试的条件标志。

根据要求组装:

.globl f
    .type   f, @function
f:
    pushl   %ebp
    xorl    %eax, %eax
    movl    %esp, %ebp
    movl    12(%ebp), %edx
    testl   %edx, %edx
    sete    %al
    addl    %edx, %eax
    movl    8(%ebp), %edx
    movl    %eax, %ecx
    popl    %ebp
    movl    %edx, %eax
    sarl    $31, %edx
    idivl   %ecx
    ret

由于事实证明这是一个如此受欢迎的问题和答案,我将详细说明一下。上面的示例基于编译器识别的编程习惯。在上述情况下,整数运算中使用了布尔表达式,并且为此目的在硬件中发明了条件标志的使用。一般来说,条件标志只能在 C 中通过使用习语访问。这就是为什么在不诉诸(内联)汇编的情况下很难用 C 语言制作一个可移植的多精度整数库。我的猜测是大多数体面的编译器都会理解上面的习语。

另一种避免分支的方法,正如在上面的一些 cmets 中所指出的那样,是谓词执行。因此,我采用了 philipp 的第一个代码和我的代码,并通过 ARM 的编译器和用于 ARM 架构的 GCC 编译器运行它,该编译器具有预测执行功能。两个编译器都避免了两个代码示例中的分支:

带有 ARM 编译器的 Philipp 版本:

f PROC
        CMP      r1,#0
        BNE      __aeabi_idivmod
        MOVEQ    r0,#0
        BX       lr

Philipp 的 GCC 版本:

f:
        subs    r3, r1, #0
        str     lr, [sp, #-4]!
        moveq   r0, r3
        ldreq   pc, [sp], #4
        bl      __divsi3
        ldr     pc, [sp], #4

我的 ARM 编译器代码:

f PROC
        RSBS     r2,r1,#1
        MOVCC    r2,#0
        ADD      r1,r1,r2
        B        __aeabi_idivmod

我的 GCC 代码:

f:
        str     lr, [sp, #-4]!
        cmp     r1, #0
        addeq   r1, r1, #1
        bl      __divsi3
        ldr     pc, [sp], #4

所有版本仍然需要分支到除法例程,因为这个版本的 ARM 没有用于除法的硬件,但 y == 0 的测试完全通过谓词执行实现。

【讨论】:

你能告诉我们生成的汇编代码吗?或者你是怎么确定没有分支的? 太棒了。可以做成constexpr 并避免像这样不必要的类型转换:template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) return t/(u+!u); 如果你想要255(lhs)/(rhs+!rhs) & -!rhs @leemes 但我的意思是| 不是&。糟糕——如果rhs0( (lhs)/(rhs+!rhs) ) | -!rhs 应该将您的值设置为0xFFFFFFF,如果rhs!=0 则设置lhs/rhs 很好的答案!我通常会为这类事情求助于组装,但维护起来总是很糟糕(更不用说便携性了;))。 确保在实际使用之前进行基准测试——可以很好预测的分支在现代 CPU 中几乎是免费的,因此避免分支的技巧最终可能会损害性能。尤其是在涉及诸如分裂之类的繁重操作的情况下。 +1 的好答案。【参考方案2】:

以下是一些具体数字,在使用 GCC 4.7.2 的 Windows 上:

#include <stdio.h>
#include <stdlib.h>

int main()

  unsigned int result = 0;
  for (int n = -500000000; n != 500000000; n++)
  
    int d = -1;
    for (int i = 0; i != ITERATIONS; i++)
      d &= rand();

#if CHECK == 0
    if (d == 0) result++;
#elif CHECK == 1
    result += n / d;
#elif CHECK == 2
    result += n / (d + !d);
#elif CHECK == 3
    result += d == 0 ? 0 : n / d;
#elif CHECK == 4
    result += d == 0 ? 1 : n / d;
#elif CHECK == 5
    if (d != 0) result += n / d;
#endif
  
  printf("%u\n", result);

请注意,我故意不调用srand(),因此rand() 总是返回完全相同的结果。另请注意,-DCHECK=0 仅计算零,因此很明显出现的频率。

现在,以各种方式编译和计时:

$ for it in 0 1 2 3 4 5; do for ch in 0 1 2 3 4 5; do gcc test.cc -o test -O -DITERATIONS=$it -DCHECK=$ch &&  time=`time ./test`; echo "Iterations $it, check $ch: exit status $?, output $time"; ; done; done

显示可以在表格中汇总的输出:

Iterations → | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.612s | -        | -        | -         | -         | -
Check 2      | 0m0.612s | 0m6.527s | 0m9.718s | 0m13.464s | 0m18.422s | 0m22.871s
Check 3      | 0m0.616s | 0m5.601s | 0m8.954s | 0m13.211s | 0m19.579s | 0m25.389s
Check 4      | 0m0.611s | 0m5.570s | 0m9.030s | 0m13.544s | 0m19.393s | 0m25.081s
Check 5      | 0m0.612s | 0m5.627s | 0m9.322s | 0m14.218s | 0m19.576s | 0m25.443s

如果零很少见,-DCHECK=2 版本的性能很差。随着零开始出现更多,-DCHECK=2 案例开始表现得更好。在其他选项中,确实没有太大区别。

但对于-O3,情况就不同了:

Iterations → | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.646s | -        | -        | -         | -         | -
Check 2      | 0m0.654s | 0m5.670s | 0m9.905s | 0m14.238s | 0m17.520s | 0m22.101s
Check 3      | 0m0.647s | 0m5.611s | 0m9.085s | 0m13.626s | 0m18.679s | 0m25.513s
Check 4      | 0m0.649s | 0m5.381s | 0m9.117s | 0m13.692s | 0m18.878s | 0m25.354s
Check 5      | 0m0.649s | 0m6.178s | 0m9.032s | 0m13.783s | 0m18.593s | 0m25.377s

在那里,检查 2 与其他检查相比没有缺点,并且随着零变得越来越普遍,它确实保留了好处。

不过,您应该真正测量一下您的编译器和您的代表性样本数据会发生什么。

【讨论】:

随机将 50% 的条目设为d=0,而不是几乎总是设为d!=0,您会看到更多的分支预测失败。如果一个分支几乎总是被跟随,或者如果一个分支或另一个分支的跟随真的是块状的,那么分支预测就很棒...... @Yakk d 迭代是内部循环,因此 d == 0 案例分布均匀。并且使 50% 的案例d == 0 现实吗? 使0.002% 的案例d==0 现实吗?它们分布在您的 d==0 案例中,每 65000 次迭代。虽然50% 可能不会经常发生,但10%1% 很容易发生,甚至90%99%。显示的测试仅真正测试“如果您基本上从未下过分支,分支预测是否会使删除分支毫无意义?”,答案是“是的,但这并不有趣”。 不,因为噪音会导致差异实际上是不可见的。 零的分布与提问者情况中的分布无关。包含 0 alpha 和其他混合的图像具有孔洞或不规则形状,但(通常)这不是噪声。假设您对数据一无所知(并认为它是噪音)是错误的。这是一个真实世界的应用程序,其中包含可能具有 0 alpha 的实际图像。而且由于一行像素可能全部 a=0 或全部 a>0,因此利用分支预测可能是最快的,尤其是当 a=0 发生很多并且(慢)除法(15+ 个周期)时!) 被避免。【参考方案3】:

如果不了解平台,就无法知道确切最有效的方法,但是,在通用系统上,这可能接近最优(使用 Intel 汇编器语法):

(假设除数在ecx,被除数在eax

mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx

四个无分支的单周期指令加上除法。商将在eax 中,余数将在最后的edx 中。 (这说明了为什么你不想派编译器去做人的工作)。

【讨论】:

这不会做除数,它只会污染除数,因此不可能除以零 @Jens Timmerman 抱歉,我是在添加 div 语句之前写的。我已经更新了文字。【参考方案4】:

根据这个link,你可以用sigaction()来阻塞SIGFPE信号(我自己没有试过,但我相信它应该可以工作)。

如果除以零错误极为罕见,这是可能的最快方法:您只需为除以零付费,而不为有效除数付费,正常执行路径根本不会改变。

但是,操作系统将涉及每个被忽略的异常,这很昂贵。我认为,您应该忽略每个除以零的至少一千个好的除法。如果异常比这更频繁,您可能会通过忽略异常而不是在除法之前检查每个值付出更多的代价。

【讨论】:

以上是关于无论结果如何,支持除以零的最快整数除法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

python的//是啥

C++的大数除法最快速度的算法

在python中//是啥意思?

整数与浮点除法 -> 谁负责提供结果?

辗转相除法求两数的最大公约数的原理是啥?

大整数除法 - Knuth 算法 D