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

概要:

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

(int) x / (int) y 

没有得到y==0的exception。 相反,我只是想要一个任意的结果。


背景:

在对image processingalgorithm进行编码时,我经常需要除以(累加的)alpha值。 最简单的变体是整数算术的纯C代码。 我的问题是,我通常得到一个由零错误除alpha==0结果像素alpha==0 。 然而,这正是结果完全不重要的像素:我不关心alpha==0像素的颜色值。


细节:

我正在寻找像这样的东西:

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

要么

 result = x / MAX( y, 1 ); 

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

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

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

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

我想最后的问题是:什么是最快的位扭曲黑客更改为0任何其他整数值,而保持所有其他值不变?


澄清

我不是100%确定分支太贵了。 但是,使用不同的编译器,所以我更喜欢基准testing,而且几乎没有优化(这确实是有问题的)。

当然,编译器在编译时很棒,但是我不能在C中expression“不关心”的结果,所以编译器将永远无法使用全面的优化。

代码应该完全兼容C,主要平台是Linux 64位gcc&clang和MacOS。

受到一些评论的启发,我摆脱了我的奔腾和gcc编译器使用的分支

 int f (int x, int y) { y += y == 0; return x/y; } 

编译器基本上认识到它可以在添加中使用testing的条件标志。

根据要求的程序集:

 .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 

由于这是一个非常受欢迎的问题和答案,我将详细阐述一下。 上面的例子是基于编译器识别的编程习惯用法。 在上述情况下,在积分算术中使用布尔expression式,并且为此目的在硬件中发明使用条件标志。 通常条件标志只能通过使用惯用法在C中访问。 这就是为什么很难在C中创build一个可移植的多精度整型库而不使用(内联)汇编。 我的猜测是,大多数体面的编译器会理解上面的成语。

另外一种避免分支的方法,就像在上面的一些评论中也提到的那样,是预测性的执行。 因此,我采取了菲利普的第一个代码和我的代码,并通过ARM的编译器和ARM体系结构的GCC编译器运行它,该编译器具有预测执行function。 两个编译器都避免了这两个代码示例中的分支:

使用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的testing是通过预测执行完全实现的。

这里是一些具体的数字,在使用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 

如果零很less, -DCHECK=2版本performance不佳。 随着零开始出现更多, -DCHECK=2情况开始performance更好。 在其他选项中,确实没有太大的区别。

然而,对于-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与其他支票相比没有缺点,而且它保持零利益的现象更为普遍。

尽pipe如此,你应该真的测量一下你的编译器和你的代表性的样本数据。

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

(假定除数在ecx ,股息在eax

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

四个非分支的单周期指令加上鸿沟。 商将在eax ,其余的将在edx中结束。 (这种显示为什么你不想发送一个编译器来做一个人的工作)。

根据这个链接 ,你可以用sigaction()来阻止SIGFPE信号(我没有尝试过,但我相信它应该可以工作)。

如果除以零错误非常罕见,则这是最快的方法:您只需为零支付分支,而不是有效分支,正常执行path根本不会改变。

但是,操作系统将涉及到每个被忽略的exception,这是非常昂贵的。 我认为,你应该至less有一个零分的好分部,你忽略。 如果例外情况比这更频繁,那么您可能会通过忽略例外情况而不是在分组之前检查每个值来支付更多的费用。