存在算术右位移位的实际用例
Which real use cases exist for arithmetic right bit shifting?
我偶然发现了一个问题,问您是否必须在实际项目中使用位移位。我已经在许多项目中广泛地使用了位移位,然而,我从来没有使用过算术位移位,即位移位,其中左操作数可以是负的,符号位应该移位而不是零。例如,在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
(其中x
是int
)的所有出现替换为-(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
对值进行符号扩展以与它一起工作 - 在使用有符号位字段时还需要有符号扩展
Godbolt的演示。移位是由编译器生成的,但通常你不使用位域(因为它们不可移植),而是对原始整数值进行操作,所以你需要自己做算术移位来提取字段struct bitfield { int x: 15; int y: 12; int z: 5; }; int f(bitfield b) { return (b.x/8 + b.y/5) * b.z; }
- 另一个例子:在x86-64中对指针进行符号扩展以获得规范地址。这用于在指针:
char* pointer = (char*)((intptr_t)address << 16 >> 16)
中存储额外的数据。你可以把它想象成底部的一个48位的位域 - V8引擎的SMI优化将值存储在前31位,因此需要右移来恢复有符号整数
- 大多数情况下,您只处理C中可用的类型,当将较窄的类型转换/提升为较宽的类型(如short到int)时,编译器会自动进行符号扩展,因此您可能不会注意到它,但是在底层,如果体系结构没有符号扩展指令,则会使用从左到右的移位。为"odd"你必须手动进行符号扩展的位数,所以这将是更常见的。例如,如果将10位像素或ADC值读入16位寄存器的顶部,
-
在转换为乘法时正确舍入符号除法,例如
上面的bitfield的输出程序集中进行除法处理的。x/12
将被优化为x*43691 >> 19
,并增加一些舍入。当然,你永远不会在普通的标量代码中这样做,因为编译器已经为你做了,但有时你可能需要对代码进行矢量化,或者制作一些相关的库,然后你需要用算术移位自己计算四舍五入。您可以看到编译器是如何在 -
饱和移位或移位大于位宽,即当移位计数>=位宽
时该值变为零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语言中,当编写设备驱动程序时,位移位运算符被广泛使用,因为位被用作需要打开和关闭的开关。位移位允许一个人容易和正确地瞄准正确的开关。
许多散列和加密函数使用位移位。看看《雇佣兵旋风》
最后,有时使用位域来包含状态信息是有用的。包括位移位在内的位操作函数对这些事情很有用。
- C++模板来检查友元函数的存在
- 既然存在危险,为什么项目要使用-I include开关
- 我们可以访问一个不存在的联盟的成员吗
- C++:对不存在的命名空间使用命名空间指令
- C++quit()函数中可能存在作用域问题
- C++擦除(如果存在)
- g++ 说函数不存在,即使包含正确的标头
- 这个极客对极客的trie实现是否存在内存泄漏问题
- 有了gcc,是否可以链接库,但前提是它存在
- C++LinkedList问题.数据类型之间存在冲突?没有匹配的构造函数
- gcc和clang在表达式是否为常量求值的问题上存在分歧
- C++Builder中的OnClick事件签名存在问题
- 如何正确地将分支添加到已存在的树中
- 我知道函数调用中存在歧义.有没有办法调用foo()函数
- 如何检查QList中是否存在值
- 根据某个函数是否存在启用模板
- 如何将分支添加到已存在的TTree:ROOT
- 地图计数确实很重要,或者只是检查是否存在
- 通用C++/Python 多语言的存在
- 为什么我的共享库中存在展开符号