存在算术右位移位的实际用例

Which real use cases exist for arithmetic right bit shifting?

本文关键字:存在      更新时间:2023-10-16

我偶然发现了一个问题,问您是否必须在实际项目中使用位移位。我已经在许多项目中广泛地使用了位移位,然而,我从来没有使用过算术位移位,即位移位,其中左操作数可以是负的,符号位应该移位而不是零。例如,在Java中,您将使用>>操作符进行算术位移位(而>>>将执行逻辑移位)。经过深思熟虑,我得出结论,我从来没有使用过可能为负的左操作数的>>

如本文所述,算术移位甚至是在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

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

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

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

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

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

它以前对我很有用,在创建掩码时,在操作位字段时使用'&'或'|'操作符,用于按位数据打包或按位图形。

我没有方便的代码示例,但我确实记得多年前在黑白图形中使用该技术来放大(通过扩展1或0位)。对于3倍缩放,'0'将变成'000','1'将变成'111',而无需知道该位的初始值。待扩展的位将被放置在高阶位置,然后算术右移将扩展它,无论它是0还是1。逻辑移位,向左或向右,总是带来零来填充空的位位置。在这种情况下,符号位是解决方案的关键。

下面是一个函数的例子,它将找到大于或等于输入的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);
}

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

  • 符号扩展:

    • 大多数情况下,您只处理C中可用的类型,当将较窄的类型转换/提升为较宽的类型(如short到int)时,编译器会自动进行符号扩展,因此您可能不会注意到它,但是在底层,如果体系结构没有符号扩展指令,则会使用从左到右的移位。为"odd"你必须手动进行符号扩展的位数,所以这将是更常见的。例如,如果将10位像素或ADC值读入16位寄存器的顶部,value >> 6将把这些位移到较低的10位位置并进行符号扩展以保留该值。如果它们被读入低10位,前6位为零,则使用value << 6 >> 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;
      }
      
      Godbolt的演示。移位是由编译器生成的,但通常你不使用位域(因为它们不可移植),而是对原始整数值进行操作,所以你需要自己做算术移位来提取字段
    • 另一个例子:在x86-64中对指针进行符号扩展以获得规范地址。这用于在指针:char* pointer = (char*)((intptr_t)address << 16 >> 16)中存储额外的数据。你可以把它想象成底部的一个48位的位域
    • V8引擎的SMI优化将值存储在前31位,因此需要右移来恢复有符号整数
  • 在转换为乘法时正确舍入符号除法,例如x/12将被优化为x*43691 >> 19,并增加一些舍入。当然,你永远不会在普通的标量代码中这样做,因为编译器已经为你做了,但有时你可能需要对代码进行矢量化,或者制作一些相关的库,然后你需要用算术移位自己计算四舍五入。您可以看到编译器是如何在

    上面的bitfield的输出程序集中进行除法处理的。
  • 饱和移位或移位大于位宽,即当移位计数>=位宽

    时该值变为零
    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页面上看到很多有条件地做某事的方法。其中大多数是通过生成全1或全0的掩码来完成的。掩码通常通过传播减法的符号位来计算,例如(x - y) >> 31(对于32位整数)。当然,它可以改为-(unsigned(x - y) >> 31),但这需要2的补充,需要更多的操作。下面是获得两个整数的最小值和最大值而不进行分支的方法:

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

    又如计算模除(1 <<)中的m = m & -((signed)(m - d) >> s);;S) - 1并行,不带除法运算符

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

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

希望对你有帮助。

但是你要小心负数

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

许多散列和加密函数使用位移位。看看《雇佣兵旋风》

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