为啥 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类型数据相除为啥可以除以零
来自“Bit Twiddling Hacks”的 SWAR 字节计数方法——它们为啥有效?