来源我的答案:
Is this expression correct in C preprocessor
我有点不在这里,而且我正在努力了解这种特殊优化是如何运作的。
正如答案中所提到的,gcc将整数除法优化为7:
mov edx, -1840700269
mov eax, edi
imul edx
lea eax, [rdx+rdi]
sar eax, 2
sar edi, 31
sub eax, edi
将其翻译为C:
int32_t divideBySeven(int32_t num) {
int32_t temp = ((int64_t)num * -015555555555) >> 32;
temp = (temp + num) >> 2;
return (temp - (num >> 31));
}
让我们来看看第一部分:
int32_t temp = ((int64_t)num * -015555555555) >> 32;
为什么这个号码?
好吧,让我们取2 ^ 64并将其除以7并看看弹出的是什么。
2^64 / 7 = 2635249153387078802.28571428571428571429
这看起来像一团糟,如果我们把它转换成八进制怎么办?
0222222222222222222222.22222222222222222222222
这是一个非常漂亮的重复模式,当然这不是巧合。我的意思是我们记得7是0b111
并且我们知道当我们除以99时我们倾向于在基数10中得到重复模式。因此,当我们除以时,我们在基数8中得到重复模式是有道理的。 7。
那么我们的号码在哪里?
(int32_t)-1840700269
与(uint_32t)2454267027
* 7 = 17179869189
最后17179869184是2^34
这意味着17179869189是7 2 ^ 34的最接近倍数。换句话说, 2454267027是适合uint32_t
的最大数字,当乘以7非常接近2的幂
八进制数是多少?
0222222222223
为什么这很重要?好吧,我们想要除以7.这个数字是2 ^ 34/7 ...大约。因此,如果我们乘以它,然后leftshift 34次,我们应该得到一个非常接近确切数字的数字。
最后两行看起来像是用来修补近似误差。
也许在这个领域拥有更多知识和/或专业知识的人可以参与其中。
>>> magic = 2454267027
>>> def div7(a):
... if (int(magic * a >> 34) != a // 7):
... return 0
... return 1
...
>>> for a in xrange(2**31, 2**32):
... if (not div7(a)):
... print "%s fails" % a
...
失败从3435973841开始,这很有趣0b11001100110011001100110011010001
对近似失败的原因进行分类有点超出我的意义,为什么补丁修复它也是如此。有谁知道魔法是如何超越我在这里放下的?
答案 0 :(得分:9)
算法的第一部分乘以7的倒数的近似值。在这种情况下,我们近似用整数乘法和右位移计算倒数。
首先,我们将值-1840700269
(八进制-015555555555
)视为32位整数。如果将其读作无符号32位整数,则其值为2454267027
(八进制22222222223
)。事实证明2454267027 / 2^34
是1/7
非常接近的整数近似值。
为什么我们选择这个数字和2的特定功率?我们使用的整数越大,近似值就越接近。在这种情况下,2454267027
似乎是最大的整数(满足上述属性),您可以使用该整数乘以有符号的32位int而不会溢出64位int。
接下来,如果我们立即右移>> 34
并将结果存储在32位int中,我们将失去两个最低位的精度。这些位是确定整数除法的右下限所必需的。
我不确定第二行是否已从x86代码中正确转换。此时,temp
约为num * 4/7
,因此num * 4/7 + num
对此进行num * 1/7 + num * 1/4
并且位变换将为您提供大约57 // 7 = 8
,这是一个非常大的错误。
例如,将57 * 2454267027 = 139893220539
作为输入57。我在代码中验证了以下内容:
139893220539 >> 32 = 32
57 * 4/7 = 32.5714...
(此时约为32 + 57 = 89
)89 >> 2 = 22
8
(呵呵?此时此处已接近-1
。)无论如何,对于最后一行,这是我们在以这种方式计算有符号整数除法之后进行的调整。我引用了Hacker对签约部门的喜悦部分:
代码最自然地计算分区结果,所以我们需要 校正使其计算传统截断为0 结果。这可以通过三个计算指令来完成 如果股息是负数,则增加股息。
在这种情况下(指你的其他帖子),你似乎正在进行签名班次,所以在负数的情况下它会减去+1
;给出{{1}}的结果。
这甚至都不能做到;这是一个更加疯狂的blog post about how to divide by 7 with just a single multiplication。