为什么0x55555556除以3 hack工作?

use*_*520 7 algorithm bit-manipulation arithmetic-expressions

有一个(相对)众所周知的黑客将32位数字除以3.而不是使用实际昂贵的除法,数字可以乘以幻数0x55555556,结果的高32位是我们正在寻找的.例如,以下C代码:

int32_t div3(int32_t x)
{
    return x / 3;
}
Run Code Online (Sandbox Code Playgroud)

与GCC一起编译-O2,结果如下:

08048460 <div3>:
 8048460:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]
 8048464:   ba 56 55 55 55          mov    edx,0x55555556
 8048469:   89 c8                   mov    eax,ecx
 804846b:   c1 f9 1f                sar    ecx,0x1f
 804846e:   f7 ea                   imul   edx
 8048470:   89 d0                   mov    eax,edx
 8048472:   29 c8                   sub    eax,ecx
 8048474:   c3                      ret 
Run Code Online (Sandbox Code Playgroud)

我猜这个sub指令负责修正负数,因为如果参数是负数,它的作用基本上是加1,NOP否则就是.

为什么这有效呢?我一直试图手动将较小的数字乘以这个掩码的1字节版本,但我看不到一个模式,我无法在任何地方找到任何解释.它似乎是一个神秘的魔术数字,其起源并不为任何人所知,就像0x5f3759df一样.

有人可以解释这背后的算法吗?

Mar*_*som 9

这是因为0x55555556真的0x100000000 / 3,四舍五入.

四舍五入很重要.由于0x100000000不均匀地除以3,因此完整的64位结果将出错.如果该错误为负,则截断低32位后的结果将太低.通过向上舍入,错误是正的,并且它全部在低32位中,因此截断将其擦除.

  • @DmitryMarchuk乘以"0x100000000"与向左移位32位相同.因此,您可以在一次操作中有效地向左移动,然后进行分割.然后向右移动(即取高32位)以获得最终结果. (4认同)
  • @szczurcio我们知道乘数中的误差是2/3,因为这是我们添加到它的多少.乘法结果中的误差将在"0*2/3"(即0)和"0xffffffff*2/3"(即0xaaaaaaa)之间.由于0xaaaaaaaab小于0x100000000,我们知道它不会溢出到高位.我应该提到这只适用于正数,GCC编译器编写者显然已经完善了我的内容. (3认同)