为啥 0x55555556 除以 3 hack 工作?

Posted

技术标签:

【中文标题】为啥 0x55555556 除以 3 hack 工作?【英文标题】:Why does the 0x55555556 divide by 3 hack work?为什么 0x55555556 除以 3 hack 工作? 【发布时间】:2016-07-02 13:25:04 【问题描述】:

有一个(相对)众所周知的将 32 位数字除以三的技巧。这个数字可以乘以幻数0x55555556,而不是使用实际昂贵的除法,结果的高 32 位就是我们要寻找的。比如下面的C代码:

int32_t div3(int32_t x)

    return x / 3;

使用 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 

我猜sub 指令负责修复负数,因为它实际上是在参数为负时加 1,否则为 NOP

但是为什么这行得通?我一直在尝试手动将较小的数字乘以这个掩码的 1 字节版本,但我看不到模式,而且我在任何地方都找不到任何解释。这似乎是一个神秘的魔法数字,任何人都不清楚其来源,就像0x5f3759df一样。

有人能解释一下这背后的算法吗?

【问题讨论】:

Faster integer division when denominator is known?的可能重复 @PeterO。请告诉我在那个问题(或答案)中,我上面概述的特定算法在哪里得到了解释。 【参考方案1】:

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

四舍五入很重要。由于0x100000000 不会被 3 整除,所以在完整的 64 位结果中会出现错误。如果该错误为负数,则截断低 32 位后的结果将太低。通过四舍五入,错误是正数,并且都在低 32 位中,因此截断会消除它。

【讨论】:

我不明白。你能进一步解释一下吗? @DmitryMarchuk 乘以 0x100000000 与左移 32 位相同。因此,您实际上是在一次操作中向左移动,然后进行除法。然后右移(即取高 32 位)以获得最终结果。 另见***.com/a/2616214/404501(乘法和移位的整数除法) 您能否详细说明向上与向下舍入的问题? “如果该错误为负数,则截断低 32 位后的结果将太低。通过四舍五入,错误为正,并且全部在低 32 位中,因此截断将其消除。” - 如果我们四舍五入,我们怎么知道高 32 位不会包含大于实际结果的值? @szczurcio 我们知道乘数中的误差是 2/3,因为这是我们为了四舍五入而加起来的。乘法结果中的误差将在0*2/3(即0)和0xffffffff*2/3(即0xaaaaaaab)之间。由于 0xaaaaaaab 小于 0x100000000,我们知道它不会溢出到高位。我应该提到这仅适用于正数,GCC 编译器作者显然已经完善了我在这里的内容。

以上是关于为啥 0x55555556 除以 3 hack 工作?的主要内容,如果未能解决你的问题,请参考以下文章

在java中的double和float类型数据相除为啥可以除以零

[Hack The Box]靶机3 Lame

来自“Bit Twiddling Hacks”的 SWAR 字节计数方法——它们为啥有效?

[Hack The Box]靶机3 Lame

在 JavaScript 中,为啥零除以零返回 NaN,而任何其他除以零返回 Infinity?

样本方差为啥要除n-1,而不是n