14

来源我的答案:

这个表达式在 C 预处理器中是否正确

我在这里有点不擅长,我试图了解这种特殊优化是如何工作的。

如答案中所述,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 为底的重复模式。因此,当我们除以 7 时,我们会在以 8 为基数的重复模式是有道理的。

那么我们的号码是从哪里来的呢?

(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... 大约。因此,如果我们乘以它,然后左移 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

对逼近失败的原因进行分类有点超出我的理解,以及补丁修复它的原因也是如此。有谁知道除了我在这里放下的魔法之外,魔法是如何运作的吗?

4

1 回答 1

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 作为输入,其中57 // 7 = 8. 我也在代码中验证了以下内容:

  • 57 * 2454267027 = 139893220539
  • 139893220539 >> 32 = 32(大约57 * 4/7 = 32.5714...在这一点上)
  • 32 + 57 = 89
  • 89 >> 2 = 22(嗯??现在还差得远8。)

无论如何,对于最后一行,这是我们在以这种方式计算有符号整数除法后所做的调整。我引用了 Hacker 对已签约部门的喜悦部分:

该代码最自然地计算了地板除法结果,因此我们需要进行更正以使其计算常规截断为 0 的结果。这可以通过三个计算指令来完成,如果被除数为负,则通过添加被除数。

在这种情况下(参考您的另一篇文章),您似乎正在做一个有符号的移位,所以它会-1在负数的情况下减去;给出 的结果+1

这甚至不是你能做的全部;这是一篇更疯狂的博客文章,介绍了如何通过一次乘法除以 7

于 2013-03-07T04:47:05.837 回答