【问题标题】:Divide a signed integer by a power of 2将有符号整数除以 2 的幂
【发布时间】:2017-02-03 03:31:15
【问题描述】:

我正在研究一种仅使用二元运算符(> + ^ ~ & | !)将有符号整数除以 2 的幂的方法,结果必须向 0 舍入。我遇到了this question 也在 Stackoverflow 关于这个问题,但是,我不明白它为什么会起作用。这是解决方案:

int divideByPowerOf2(int x, int n)
{
    return (x + ((x >> 31) & ((1 << n) + ~0))) >> n;
}

我了解x &gt;&gt; 31 部分(仅在x 为负数时添加下一部分,因为如果为正数,x 将自动向0 舍入)。但困扰我的是(1 &lt;&lt; n) + ~0 部分。它是如何工作的?

【问题讨论】:

  • 二进制补码。但你是对的,答案没有解释任何事情。现在,当您这样做时,您会遭到反对...
  • x + ~0 是写x - 1 的一种有趣方式,它只是将舍入掩码截断为n
  • 1) 担心除以负数? 2) 担心可移植地除负数? 3) 担心处理x == INT_MIN? 4) 满足/超过int 位宽的 2 的幂呢?否则,目标太窄了,超出了对int 的可能性或范围和细节的狭隘想法,它就没有多大用处。

标签: c binary bit-manipulation


【解决方案1】:

假设 2 补码,仅将被除数移位就相当于某种除法:不是传统的除法,即我们将除数四舍五入到除数的下一个倍数,趋向于零。但是另一种我们将红利四舍五入到负无穷大。我在 Smalltalk 中重新发现了这一点,请参阅 http://smallissimo.blogspot.fr/2015/03/is-bitshift-equivalent-to-division-in.html

例如,让我们将 -126 除以 8。传统上,我们会写

-126 = -15 * 8 - 6

但是如果我们向无穷大取整,我们会得到一个正余数并写下它:

-126 = -16 * 8 + 2

位移正在执行第二个操作,就位模式而言​​(为了简短起见,假设 8 位长 int):

1000|0010 >> 3 = 1111|0000
1000|0010      = 1111|0000 * 0000|1000 + 0000|0010

如果我们想要传统的除法,商向零舍入,余数与被除数相同,该怎么办?很简单,我们只需要将商加 1 - 当且仅当被除数为负且除法不精确时。

你看到x&gt;&gt;31对应第一个条件,被除数为负,假设int有32位。

如果除法不精确,则第二项对应第二个条件。

看看 -1, -2, -4, ... 如何在两个补码中编码:1111|1111、1111|1110、1111|1100。所以 2 的 n 次方的否定有 n 个尾随零。

当被除数有 n 个尾随零并且我们除以 2^n 时,则不需要在最终商中加 1。在任何其他情况下,我们都需要添加 1。

((1

最后 n 位并不重要,因为我们将向右移动并丢弃它们。因此,如果除法是精确的,则被除数的 n 个尾随位为零,我们只需添加将被跳过的 n 个 1。相反,如果除法不精确,则被除数的 n 个尾随位中的一个或多个为 1,我们肯定会导致 n+1 位位置进位:这就是我们将商加 1 的方式(我们将 2^n 添加到股息)。这是否解释得更清楚一点?

【讨论】:

    【解决方案2】:

    这是“只写代码”:不要试图理解代码,而是尝试自己创建它。

    例如,让我们将一个数字除以 8(右移 3)。 如果该数字为负数,则正常的右移会朝错误的方向舍入。让我们通过添加一个数字来“修复”它:

    int divideBy8(int x)
    {
        if (x >= 0)
            return x >> 3;
        else
            return (x + whatever) >> 3;
    }
    

    在这里,您可以为whatever 提出一个数学公式,或者进行一些反复试验。无论如何,这里whatever = 7

    int divideBy8(int x)
    {
        if (x >= 0)
            return x >> 3;
        else
            return (x + 7) >> 3;
    }
    

    如何统一这两种情况?您需要创建一个如下所示的表达式:

    (x + stuff) >> 3
    

    其中stuff 是 7 表示负 x,0 表示正 x。这里的技巧是使用x &gt;&gt; 31,这是一个32位数字,其位等于x的符号位:全0或全1。所以stuff

    (x >> 31) & 7
    

    结合所有这些,并将 8 和 7 替换为更一般的 2 的幂,您就得到了您所询问的代码。


    注意:在上面的描述中,我假设int代表一个32位的硬件寄存器,硬件使用二进制补码表示进行右移。

    【讨论】:

    • 由于xint 类型,当x 为负数时,(x &gt;&gt; 31) 可能会或可能不会导致全1。 ISO C 标准规定(例如 C99 第 6.5.7 节第 5 段)右移负值的有符号整数会调用 实现定义 行为。
    • 实现定义的行为可以通过强制转换为无符号来修复,如果它移动 0,这个解决方案也会中断。
    【解决方案3】:

    OP 的参考是 C# 代码和许多细微的差异,导致它与 C 的错误代码,因为这篇文章被标记了。

    int 不一定是 32 位的,因此使用 32 的幻数并不能提供可靠的解决方案。

    特别是(1 &lt;&lt; n) + ~0 导致实现定义的行为,当n 导致一个位被转移到符号位置时。编码不好。

    将代码限制为仅使用“二进制”运算符 &lt;&lt; &gt;&gt; + ^ ~ &amp; | ! 会鼓励编码人员假设关于 int 的事情,这既不便携也不符合 C 规范。所以 OP 发布的代码一般不会“工作”,尽管可能在许多常见的实现中工作。

    int 不是 2 的补码、不使用范围 [-2147483648 .. 2147483647]1 &lt;&lt; n 使用与预期不符的实现行为时,OP 代码会失败。

    // weak code
    int divideByPowerOf2(int x, int n) {
      return (x + ((x >> 31) & ((1 << n) + ~0))) >> n;
    }
    

    假设long long 超出int 的范围,下面是一个简单的替代方案。我怀疑这是否符合 OP 目标的某些角,但 OP 的既定目标鼓励非鲁棒编码。

    int divideByPowerOf2(int x, int n) {
      long long ill = x;
      if (x < 0) ill = -ill;
      while (n--) ill >>= 1;
      if (x < 0) ill = -ill;
      return (int) ill;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-05-05
      • 2012-09-23
      • 2011-11-13
      • 2018-06-30
      • 1970-01-01
      • 1970-01-01
      • 2021-03-14
      相关资源
      最近更新 更多