不管结果如何,最快的整数除法支持零除法?


109

摘要:

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

(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];

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

我猜最后一个问题是:将hack更改0到任何其他整数值,同时保持所有其他值不变的最快方法是什么?


澄清说明

我不是100%确信分支成本太高。但是,由于使用了不同的编译器,因此我更喜欢基准测试并没有进行任何优化(这确实是有问题的)。

可以肯定的是,编译器在处理纠结方面非常出色,但是我无法在C语言中表达“无关紧要”的结果,因此编译器将永远无法使用全部优化。

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


22
您如何确定if分支过于昂贵?
djechlin

7
你如何确定存在一个分支?
leemes

13
+1进行概要分析,借助现代分支预测,您可能不需要此功能。另外,为什么还要编码自己的图像处理算法?
TC1

8
“最快的事情是什么?”也许y += !y?无需分支即可进行计算。你可以比较x / (y + !y)反对x / max(y, 1),也许还y ? (x/y) : 0。我想至少在启用优化的情况下,它们两个都不会分支。
leemes

6
任何认为现代分支预测意味着您不必这样做的人都没有描述足够的按像素级别运行的分支消除代码。如果Alpha 0部分很大且连续,则可以接受现代分支预测。有一个摆弄微优化的地方,而每个像素的操作正是那个地方。
Yakk-Adam Nevraumont 2013年

Answers:


107

受到一些评论的启发,我摆脱了奔腾和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制作可移植的多精度整数库如此困难的原因。我的猜测是,大多数体面的编译器都会理解上述习惯用法。

避免执行分支的另一种方法(如上述某些注释中所述)是谓词执行。因此,我拿了philipp的第一个代码和我的代码,并通过ARM的编译器和ARM体系结构的GCC编译器运行了该代码,该体系结构具有预先确定的执行功能。两个编译器都在两个代码示例中都避免了分支:

Philipp的带有ARM编译器的版本:

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是通过谓词执行完全实现的。


您能否向我们展示最终的汇编代码?或您如何确定没有分支?
哈茨伊(Haatschii),

1
太棒了 可制作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
Yakk -亚当Nevraumont

1
@leemes但我|不是那个意思&。糟糕- ( (lhs)/(rhs+!rhs) ) | -!rhs应该将您的值设置为0xFFFFFFFif rhsis 0lhs/rhs if rhs!=0
Yakk-Adam Nevraumont

1
这非常聪明。
Theodoros Chatzigiannakis

1
好答案!我通常会为这类事情使用汇编程序,但是维护起来总是很恐怖的(更不用说便携性了;))。
狮子座

20

以下是使用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没有缺点,并且随着零变得更加普遍,它确实保留了好处。

不过,您应该真正衡量一下编译器和代表性示例数据会发生什么。


4
使50%的条目是d=0随机的,而不是几乎总是使它变为d!=0,您会看到更多的分支预测失败。如果几乎总是遵循一个分支,或者一个分支或另一个分支的跟随确实很
笨拙

@Yakk d迭代是内部循环,因此d == 0案例分布均匀。使50%的案件d == 0现实吗?

2
正在0.002%的情况下,d==0现实吗?它们遍历您的d==0案例,每65,000次迭代分布一次。虽然50%可能不会经常发生,10%或者1%可以很容易地发生,甚至90%99%。所显示的测试只能真正测试“如果您基本上从不下过分支,分支预测是否会使删除分支变得毫无意义?”,答案是“是的,但这并不有趣”。
Yakk-Adam Nevraumont

1
不会,因为噪音会有效地消除差异。

3
零的分布与提问者情况中的分布无关。包含0 alpha和其他字母的混合图像具有孔或不规则形状,但是(通常)这不是噪点。假设您对数据一无所知(并认为它是噪声)是一个错误。这是一个实际应用,具有可能具有0 alpha的实际图像。而且由于一行像素可能全部具有a = 0或全部a> 0,因此充分利用分支谓词可能是最快的,尤其是当a = 0发生很多且(缓慢)除法(15个以上周期)时!)被避免。
DDS

13

在不知道平台的情况下,无法知道确切的最有效方法,但是,在通用系统上,这可能接近最佳方法(使用Intel汇编器语法):

(假设除数在ecx,分红在eax

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

四个独立的单周期指令加上除法。商将在其中eax,余数将在edx最后。(这种情况说明了为什么您不希望发送编译器来完成任务)。


师在哪里?
Yakk-Adam Nevraumont

1
这不会做除法运算,只会污染除数,因此不可能被零除
泰勒·德登

@Jens Timmerman抱歉,我在添加div语句之前写了该代码。我已经更新了文字。
泰勒·德登

1

根据此链接,您可以使用来阻止SIGFPE信号sigaction()(我自己没有尝试过,但我认为它应该可以工作)。

如果极少被零除错误,这是最快的方法:您只为零除法付费,而不为有效除法付费,完全不改变常规执行路径。

但是,操作系统将涉及到所有被忽略的异常,这非常昂贵。我认为,您应该忽略至少每个零除以1000的良好除法。如果异常的发生频率高于此频率,则您可能会忽略该异常,而不是检查除法之前的每个值,从而付出更多的代价。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.