【问题标题】:Which real use cases exist for arithmetic right bit shifting?算术右位移位存在哪些实际用例?
【发布时间】:2014-07-31 10:31:02
【问题描述】:

我偶然发现了一个问题,询问是否you ever had to use bit shifting in real projects。我在许多项目中都广泛使用了位移,但是,我从来不必使用算术位移,即位移​​左操作数可能是负数和符号位应移入而不是零。例如,在 Java 中,您将使用 >> 运算符进行算术位移(而 >>> 将执行逻辑移位)。经过深思熟虑,我得出的结论是,我从未使用过带有可能为负左操作数的>>

正如this answer 中所述,算术移位甚至是在 C++ 中定义的实现,因此——与 Java 相比——C++ 中甚至没有用于执行算术移位的标准化运算符。答案还指出了一个有趣的问题,即我什至不知道的负数移位问题:

+63 >> 1 = +31 (integral part of quotient E1/2E2)
00111111 >> 1 = 00011111
-63 >> 1 = -32 
11000001 >> 1 = 11100000

所以-63>>1 产生-32,这在查看位时很明显,但可能不是大多数程序员第一眼所期望的。更令人惊讶(但在查看这些位时再次明显)是 -1>>1-1,而不是 0

那么,可能负值的算术右移的具体用例是什么?

【问题讨论】:

  • 但是等等,>> 是算术移位。 >>> 是逻辑移位。
  • @harold:哦,对了,谢谢,已更正
  • 这个问题已经很老了。这些天来,一切都被投票关闭。顺便说一句,在某些情况下,算术移位对于将位转换为全角掩码很有用。
  • @Jongware:它是 int 除以二,也就是说,当您使用 a/2a 是几乎所有编程语言中的整数类型时,您会得到什么。
  • @JoopEggen 是的,它是通过将>> 转换为>>> 来解决的,所以它确实是 使用>>

标签: java c++ bit-manipulation bit-shift sign-extension


【解决方案1】:

也许最著名的是无分支绝对值

int m = x >> 31;
int abs = x + m ^ m;

它使用算术移位将符号位复制到所有位。我遇到的大多数算术移位都是这种形式。当然,这需要算术移位,您可以将所有出现的x >> 31(其中xint)替换为-(x >>> 31)

值 31 来自int 的大小(以位为单位),在 Java 中定义为 32。所以右移 31 会移出除符号位之外的所有位,符号位(因为它是算术移位)被复制到这 31 位,在每个位置留下符号位的副本。

【讨论】:

  • 虽然这里不需要算术移位(正如你自己所说的那样),但这是第一个回答我问题的答案,谢谢!
  • +1 是一个真实的例子。 +0.5 因为它很有创意。 -0.5 表示过早优化(几乎每个编译器都应该将对 abs 的调用优化为无分支指令,可能甚至比这个手工版本更快;除非你知道自己是,否则不要试图比你的编译器更聪明)
【解决方案2】:

它以前对我派上用场,用于创建蒙版,然后在“&”或“|”中使用操作位域时的运算符,用于按位数据打包或按位图形。

我没有方便的代码示例,但我记得很多年前在黑白图形中使用该技术进行放大(通过扩展一点,1 或 0)。对于 3 倍变焦,“0”将变为“000”,“1”将变为“111”,而无需知道该位的初始值。要扩展的位将被放置在高位位置,然后算术右移将扩展它,无论它是 0 还是 1。逻辑移位,无论是左移还是右移,总是带入零来填充空出的位位置.在这种情况下,符号位是解决问题的关键。

【讨论】:

  • 你能举个例子吗?
  • 我没有方便的代码示例,但我确实记得很多年前在黑白图形中使用该技术来放大(通过扩展一点,1 或 0)。对于 3 倍缩放,“0”将变为“000”,“1”将变为“111”,而无需知道该位的初始值。
  • 您描述了一种情况,您希望进行逻辑位移,而不是算术位移。在这种情况下,您应该简单地使用无符号整数来避免符号问题。
  • 不,这是明确的算术移位。要扩展的位将被放置在高位位置,然后算术右移将扩展它,无论它是 0 还是 1。逻辑移位,无论是左移还是右移,总是带入零来填充空出的位位置.在这种情况下,符号位是解决方案的关键。
【解决方案3】:

这是一个函数示例,该函数将找到大于或等于输入的 2 的最小幂。这个问题还有其他可能更快的解决方案,即任何面向硬件的解决方案或只是一系列右移和 OR。此解决方案使用算术移位来执行二进制搜索。

unsigned ClosestPowerOfTwo(unsigned num) {
  int mask = 0xFFFF0000;
  mask = (num & mask) ? (mask << 8) :  (mask >> 8);
  mask = (num & mask) ? (mask << 4) :  (mask >> 4);
  mask = (num & mask) ? (mask << 2) :  (mask >> 2);
  mask = (num & mask) ? (mask << 1) :  (mask >> 1);
  mask = (num & mask) ?  mask       :  (mask >> 1);
  return (num & mask) ? -mask       : -(mask << 1);
}

【讨论】:

    【解决方案4】:

    确实,逻辑右移更常用。然而,有许多操作需要算术移位(或者用算术移位更优雅地解决)

    • 签名扩展:

      • 大多数情况下,您只处理 C 中的可用类型,编译器会在将较窄的类型转换/提升为较宽的类型(如 short 为 int)时自动对扩展进行签名,因此您可能不会注意到它,但在如果体系结构没有符号扩展指令,请使用 left-then-right shift is used。对于“奇数”位数,您必须手动进行符号扩展,因此这将更为常见。例如,如果将 10 位像素或 ADC 值读入 16 位寄存器的高位:value &gt;&gt; 6 会将这些位移动到低 10 位位置并进行符号扩展以保留该值。如果它们被读入低 10 位而前 6 位为零,您将使用 value &lt;&lt; 6 &gt;&gt; 6 对值进行符号扩展以使用它
      • 在使用带符号位字段时还需要带符号扩展
        struct bitfield {
            int x: 15;
            int y: 12;
            int z: 5;
        };
        
        int f(bitfield b) {
            return (b.x/8 + b.y/5) * b.z;
        }
        
        Demo on Godbolt。移位由编译器生成,但通常您不使用位域(因为它们不可移植)而是对原始整数值进行操作,因此您需要自己进行算术移位以提取字段
      • 另一个示例:对指针进行符号扩展以生成canonical address in x86-64。这用于store additional data in the pointerchar* pointer = (char*)((intptr_t)address &lt;&lt; 16 &gt;&gt; 16)。您可以将其视为底部的 48 位位域
      • V8 引擎的 SMI optimization 将值存储在前 31 位,因此需要右移才能恢复有符号整数
    • Round signed division properlyconverting to a multiplication 时,例如x/12 将优化为x*43691 &gt;&gt; 19,并进行一些额外的舍入。当然,您永远不会在普通标量代码中执行此操作,因为编译器已经为您执行此操作,但有时您可能需要对代码进行矢量化或make some related libraries,然后您需要使用算术移位自己计算舍入。您可以在上面的位域的输出程序集中看到编译器如何舍入除法结果

    • 饱和移位或大于位宽的移位,即当移位计数>=位宽时值变为零

      uint32_t lsh_saturated(uint32_t x, int32_t n) // returns 0 if n == 32
      {
          return (x << (n & 0x1F)) & ((n-32) >> 5);
      }
      
      uint32_t lsh(uint32_t x, int32_t n) // returns 0 if n >= 32
      {
          return (x << (n & 0x1F)) & ((n-32) >> 31);
      }
      
    • 位掩码,在无分支选择等各种情况下很有用(即muxer)。你可以在著名的bithacks page 上看到很多有条件地做某事的方法。它们中的大多数是通过生成全 1 或全 0 的掩码来完成的。掩码通常通过传播减法的符号位来计算,例如(x - y) &gt;&gt; 31(对于 32 位整数)。当然可以改成-(unsigned(x - y) &gt;&gt; 31),但这需要2的补码,需要更多的操作。这是在不分支的情况下获取两个整数的最小值和最大值的方法:

      min = y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1)));
      max = x - ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1)));
      

      另一个例子是Compute modulus division by (1 << s) - 1 in parallel without a division operator中的m = m &amp; -((signed)(m - d) &gt;&gt; s);

    【讨论】:

      【解决方案5】:

      我不太清楚你的意思。但是,我将推测您想将位移位用作算术函数。 我看到的一件有趣的事情是二进制数的这种性质。

      int n = 4;
      int k = 1;
      
      n = n << k; // is the same as n = n * 2^k
      //now n = (4 * 2) i.e. 8
      n = n >> k; // is the same as n = n / 2^k
      //now n = (8 / 2) i.e. 4
      

      希望有帮助。

      但是是的,你要小心负数 我会屏蔽,然后相应地把它转回来

      【讨论】:

      • “但是是的,你要小心我会屏蔽的负数,然后相应地将其转回”负数是算术右移对逻辑右移产生影响的地方。对于正数算术右移 == 逻辑右移,因此您也可以使用 &gt;&gt;&gt;(在 Java 中)。我明确地寻找“数字可能为负”的情况
      • 在java中我可以假设那里是一个抽象。我不知道 c 或 c++ 中的 >>。但是如果你用否定的例子来尝试我的例子。你得到你所期望的。 -4
      • 不,没有这样的运算符。但是,我指的是左操作数可能为负数的情况。只要它总是积极的,一切都很好。另外,我说的是right shift。是的,-4 &lt;&lt; 2 =-16,但这里有一个反例:-1&gt;&gt;1 = -1 不是 0
      • @gexicide:我看不出9 &gt;&gt; 1 = 4-9 &gt;&gt; 1 = -5 之间有什么区别。右移 1丢弃一位,总是;所以它总是向下取整
      • 请注意,出于可读性原因,在实际代码中不建议使用这种算术运算。要表达您想要划分,请使用/ 运算符!即使您除以其中只有一个指数的变量的幂:n = n / (1 &lt;&lt; k)n = n &lt;&lt; k 更容易阅读(至少恕我直言)。请注意,如果该操作将更快并且结果相同,则编译器可能已针对移位操作进行了优化。出于优化原因写n &lt;&lt; k 是过早的优化,这是不好的。
      【解决方案6】:

      在 C 中编写设备驱动程序时,移位运算符被广泛使用,因为位被用作需要打开和关闭的开关。位移允许人们轻松正确地瞄准正确的开关。

      许多散列和加密函数都使用位移位。看看Mercenne Twister

      最后,使用位域来包含状态信息有时很有用。包括位移在内的位操作函数对这些事情很有用。

      【讨论】:

      • 我的问题是关于算术位移,其中左操作数可以是负数。这个问题不是关于正数或无符号数的通常移位。
      猜你喜欢
      • 1970-01-01
      • 2011-04-11
      • 1970-01-01
      • 1970-01-01
      • 2020-07-02
      • 1970-01-01
      • 2015-07-05
      • 1970-01-01
      • 2014-02-14
      相关资源
      最近更新 更多