显然,在ARM cpus中,除法比位移慢10-100倍。在this site上声明可以通过多种方式解决这个问题。其中一个是小问题的查找表,这是很好的标准。但是列出的也是乘法乘以一个定点倒数后跟一个位移(所以x/3
变成(x*6) << 1
等)另一个用(x % y) > z
取代x > (z * y)
。
我离专家很远,但这听起来很奇怪。我的意思是,如果您使用的是现代编译器,那么这不是那种针对您优化的东西吗?
答案 0 :(得分:3)
unsigned int fun1 ( unsigned int a, unsigned int b )
{
return(a/b);
}
unsigned int fun2 ( unsigned int a )
{
return(a/2);
}
unsigned int fun3 ( unsigned int a )
{
return(a/3);
}
unsigned int fun10 ( unsigned int a )
{
return(a/10);
}
unsigned int fun13 ( void )
{
return(10/13);
}
然后尝试一下。
00000000 <fun1>:
0: e92d4010 push {r4, lr}
4: ebfffffe bl 0 <__aeabi_uidiv>
8: e8bd4010 pop {r4, lr}
c: e12fff1e bx lr
00000010 <fun2>:
10: e1a000a0 lsr r0, r0, #1
14: e12fff1e bx lr
00000018 <fun3>:
18: e59f3008 ldr r3, [pc, #8] ; 28 <fun3+0x10>
1c: e0802093 umull r2, r0, r3, r0
20: e1a000a0 lsr r0, r0, #1
24: e12fff1e bx lr
28: aaaaaaab bge feaaaadc <fun13+0xfeaaaa9c>
0000002c <fun10>:
2c: e59f3008 ldr r3, [pc, #8] ; 3c <fun10+0x10>
30: e0802093 umull r2, r0, r3, r0
34: e1a001a0 lsr r0, r0, #3
38: e12fff1e bx lr
3c: cccccccd stclgt 12, cr12, [r12], {205} ; 0xcd
00000040 <fun13>:
40: e3a00000 mov r0, #0
44: e12fff1e bx lr
正如人们所料,如果编译器无法处理编译时间,那么它会调用相应的库函数,这是性能问题的根源。如果您没有原生除法指令,那么最终会执行许多指令,以及所有取指令。右边的声音慢了10到100倍。
有趣的是他们确实使用了1/3和1/10技巧,如果结果可以在编译时计算,那么只需返回固定的结果。
编译器作者可以阅读相同的Hackers Delight和Stack Overflow页面并了解相同的技巧,如果愿意和感兴趣,可以实现这些优化。不要以为他们总会这样;只是因为我有一些版本的编译器发现这些并不意味着所有编译器都可以/将会。
至于你是否应该让编译器/工具链为你做这件事:那取决于你;即使您有分割指令,如果您定位多个平台,您可以选择向右移动而不是除以2;你可以选择做其他这些技巧。如果你拥有鸿沟,那么你至少知道它在做什么;如果你把它交给编译器那么你必须经常反汇编以了解它在做什么(如果你在意)。如果这是一个时间关键部分,那么你可能希望两者都做,看看编译器做了什么,然后窃取了答案或创建自己的确定性解决方案(让它留给编译器不一定是确定性的,我认为这是重点)。
修改
arm-none-eabi-gcc -O2 -c so.c -o so.o
arm-none-eabi-objdump -D so.o
arm-none-eabi-gcc --version
arm-none-eabi-gcc (GCC) 6.3.0
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
我在这里有一个gcc 4.8.3也产生了那些优化......以及5.4.0,所以他们已经做了一段时间了。
arm UMULL
指令是64位= 32位* 32位操作,因此它不会溢出乘法。当然对于1/3和1/10并且不确定1 / N的N值有多大,你可以用64位并且任何32位操作数工作。执行一个简单的实验表明,至少对于这两种情况,所有可能的32位模式都适用于无符号模式。
似乎也使用了签名技巧:
int negfun ( int a )
{
return(a/3);
}
00000000 <negfun>:
0: e59f3008 ldr r3, [pc, #8] ; 10 <negfun+0x10>
4: e0c32390 smull r2, r3, r0, r3
8: e0430fc0 sub r0, r3, r0, asr #31
c: e12fff1e bx lr
10: 55555556 ldrbpl r5, [r5, #-1366] ; 0xfffffaaa
答案 1 :(得分:1)
除常数之外,通常由编译器优化乘法和移位序列,即使在具有除法指令的处理器上也是如此。在某些情况下,序列有点长,但仍然只使用一个乘法。链接到关于此的先前线程。
Why does GCC use multiplication by a strange number in implementing integer division?
在没有除法的处理器上除以变量通常由优化函数处理,基于此Wiki文章中提到的方法的一些变体:
http://en.wikipedia.org/wiki/Division_algorithm#Fast_division_methods
使用32位乘32位除法作为示例,可能使用3个主路径。对于除数&lt; 256,可以使用除常数法(256入口表)。对于预期商数&lt;在图256中,可以使用展开的减法和移位序列。主路径进行表查找以获得初始近似,然后是包含4次乘法的序列,一些加法,减法,并且将估计商中的表值中的正确位数转换为四倍,使得估计商=实际商或者实际商 - 1.然后从被除数中减去估计商*除数的乘积,如果余数> =除数,则递增商并从除数中减去除数。对于64位乘64位除法,主序列将涉及6次乘法,...以产生估计商。