What is the fastest integer division supporting division by zero no matter what the result is?
总结:
我在找最快的计算方法
1 | (int) x / (int) y |
没有得到EDOCX1的例外情况。相反,我只是想要一个任意的结果。
背景:
在编码图像处理算法时,我经常需要除以(累计的)alpha值。最简单的变量是带整数算术的纯C代码。我的问题是,对于使用
细节:
我在找类似的东西:
1 | result = (y==0)? 0 : x/y; |
或
1 | result = x / MAX( y, 1 ); |
x和y是正整数。代码在嵌套循环中执行了大量次,因此我正在寻找一种消除条件分支的方法。
当y不超过字节范围时,我对这个解决方案很满意
1 2 3 | unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 }; [...] result = x / kill_zero_table[y]; |
但这显然不适用于更大的范围。
我想最后一个问题是:在保持所有其他值不变的情况下,将0更改为任何其他整数值的最快位是什么?
澄清
我不能百分之百肯定分支太贵了。但是,使用了不同的编译器,所以我更喜欢使用很少优化的基准测试(这确实有问题)。
当然,编译器在进行位旋转时是非常好的,但是我不能用C来表示"不关心"的结果,因此编译器永远无法使用全范围的优化。
代码应该是完全C兼容的,主要平台是带有gcc&clang和macos的64位Linux。
受到一些评论的启发,我去掉了Pentium和
1 2 3 4 5 | int f (int x, int y) { y += y == 0; return x/y; } |
编译器基本上认识到它还可以使用测试的条件标志。
根据要求,组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | .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编译器中运行它,后者具有谓词执行功能。两个编译器都避免在两个代码示例中使用分支:
带ARM编译器的Philipp版本:
1 2 3 4 5 | f PROC CMP r1,#0 BNE __aeabi_idivmod MOVEQ r0,#0 BX lr |
Philipp与GCC的版本:
1 2 3 4 5 6 7 | f: subs r3, r1, #0 str lr, [sp, #-4]! moveq r0, r3 ldreq pc, [sp], #4 bl __divsi3 ldr pc, [sp], #4 |
我的ARM编译器代码:
1 2 3 4 5 | f PROC RSBS r2,r1,#1 MOVCC r2,#0 ADD r1,r1,r2 B __aeabi_idivmod |
我的GCC代码:
1 2 3 4 5 6 | f: str lr, [sp, #-4]! cmp r1, #0 addeq r1, r1, #1 bl __divsi3 ldr pc, [sp], #4 |
所有版本仍然需要一个到划分例程的分支,因为这个版本的ARM没有用于划分的硬件,但是
下面是一些具体的数字,在使用GCC4.7.2的窗口上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #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 ", result); } |
注意,我故意不调用
现在,通过各种方式对其进行编译和计时:
1 | $ 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 |
显示可在表中汇总的输出:
1 2 3 4 5 6 7 8 | 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 |
如果零很少出现,那么
不过,对于
1 2 3 4 5 6 7 8 | 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没有任何缺点,而且由于零变得更常见,它确实保留了好处。
不过,您确实应该测量一下编译器和代表性示例数据会发生什么。
在不了解平台的情况下,无法确切了解最有效的方法,但是,在通用系统上,这可能接近最佳方法(使用英特尔汇编程序语法):
(假设除数在
1 2 3 4 5 | mov ebx, ecx neg ebx sbb ebx, ebx add ecx, ebx div eax, ecx |
四个不分枝的单周期指令加上除法。商将以
根据这个链接,你可以用
如果除数为零的错误非常罕见,那么这是最快的方法:您只需为除数为零付费,而不是为有效的除数付费,正常的执行路径根本不会更改。
然而,操作系统将涉及到每一个被忽略的异常,这是昂贵的。我认为,你应该有至少一千个好的除法,每除法零,你忽略了。如果异常比这更频繁,那么您可能会因为忽略异常而付出更多的代价,而不是在除法之前检查每个值。