算术右移位有哪些实际用例?

时间:2014-07-31 10:31:02

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

我偶然发现了一个询问是否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

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

6 个答案:

答案 0 :(得分:6)

最着名的可能是无分支绝对值

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

使用算术移位将signbit复制到所有位。我遇到的大多数算术移位用途都是那种形式。当然,算术转移不是必需的,你可以用x >> 31替换所有出现的x(其中int-(x >>> 31)

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

答案 1 :(得分:1)

之前,它已经派上用场了,用于制作面具然后用于'&'或'|'操作位字段时的运算符,用于按位数据打包或按位图形。

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

答案 2 :(得分:0)

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

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
希望有所帮助。

但是,是的,你要小心负数 我会掩盖,然后相应地将其转回来

答案 3 :(得分:0)

这是一个函数的例子,它将找到大于或等于输入的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 :(得分:0)

实际上逻辑右移更为常用。但是,有许多操作需要算术移位(或者用算术移位可以更优雅地解决)

  • 签名扩展名:

    • 大多数时候,您只处理C语言中的可用类型,并且在将较窄的类型强制转换/提升为较宽的类型时(例如,从int转换为int),编译器会自动对扩展符号进行签名,因此您可能不会注意到它,但是如果该体系结构没有用于符号扩展的指令,请使用left-then-right shift is used。对于“奇数”位数,您将必须手动进行符号扩展,因此这将更为常见。例如,如果将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;
      }
      
      Demo on Godbolt。移位是由编译器生成的,但是通常您不使用位域(因为它们不可移植),而是对原始整数值进行操作,因此您需要自己进行算术移位以提取字段
    • 另一个例子:将指针符号扩展为canonical address in x86-64。这用于store additional data in the pointerchar* pointer = (char*)((intptr_t)address << 16 >> 16)。您可以将其视为底部的48位位域
    • V8引擎的SMI optimization将值存储在前31位,因此需要右移以恢复带符号的整数
  • Round signed division properly,当converting to a multiplication(例如x/12)将被优化为x*43691 >> 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) >> 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)));
    

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

答案 5 :(得分:-1)

在C中编写器件驱动程序时,由于位被用作需要打开和关闭的开关,因此广泛使用位移操作符。位移允许人们轻松正确地定位正确的开关。

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

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