移位运算

Posted Zhaoxi Zhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了移位运算相关的知识,希望对你有一定的参考价值。

概述

这里以C语言为例描述移位运算的行为。 对于一个位表示为 \\(x_w-1\\), \\(x_w-2\\) ,..., \\(x_0\\) 的操作数 x, C 表达式 \\(x << k\\) 会生成一个値,其位表示为 \\([\\) \\(x_w-k-1\\), \\(x_w-k-2\\) ,..., \\(x_0\\),0,... , 0 \\(]\\) 。也就是说,x 向左移动 k 位,丢弃最高的 k 位,并在右端补 k 个 0。移位量应该是一个 0 ~ w - 1 之间的值。移位运算是从左至右可结合的,所以 \\(x << j << k\\) 等价于 \\((x << j) << k\\)

有一个相应的右移运算 \\(x>>k\\),但是它的行为有点微妙。一般而言,机器文持两种形式的右移: 逻辑右移和算术右移。逻辑右移在左端补 k 个零,得到的结果是 \\([\\) 0 , ... , 0 , \\(x_w-1\\)\\(x_w-2\\) , ... ,\\(x_k\\) \\(]\\)。算术右移是在左端补 k 个最高有效位的值,得到的结果是 \\([\\) \\(x_w-1\\), ..., \\(x_w-1\\), \\(x_w-1\\)\\(x_w-2\\), ..., \\(x_k\\) \\(]\\)。这种做法看上去可能有点奇特,但是我们会发现它对有符号整数数据的运算非常有用。

让我们来看一个例子,下面的表给出了对一个8位参数 × 的两个不同的值做不同的移位操作得到的结果:

操作
参数x [011000111] [100101011]
x << 4 [00110000] [01010000]
× > 4 (逻辑右移) [00000110] [000010011]
x>>4 (算术右移) [00000110] [11111001]

可以看到除了一个条目之外,其他的都包含填充0。唯一的例外是算术右移 [10010101] 的情况。因为操作数的最高位是1,填充的値就是1。
C语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。不幸地,这就意味着任何假设一种或者另一种右移形式的代码都可能会遇到可移植性问题。然而,实际上,几乎所有的编译器/机器组合都对有符号数使用算术右移,且许多程序员也都假设机器会使用这种右移。另一方面,对于无符号数,右移必须是逻辑的。

与C相比,Java 对于如何进行右移有明确的定义。表达是 \\(x>>k\\) 会将 × 算术右移 k 个位置,而 \\(x >>> k\\) 会对 × 做逻辑右移。

逻辑右移

在 C/C++ 中如果要对有符号数进行逻辑右移,那么不能简单的使用 x >> k,因为对于负数会用符号位填充左端的 k 位。因此要进行逻辑右移,我们使用一个数来记录前 k 位,使其前 k 位都是0,其余都是1,然后与 x >> k 的结果做与运算。

// 由于无符号数右移一定是逻辑右移,因此先转换为无符号数,得到前k位都是0,然后再转换为有符号数
int logic_r_shift(int x, int k) 
    return (int)((unsigned int)x >> k);

网上还流传有另一种写法:

// mask 得到一个前 k 位都是0的数,但是由于 >> 的运算符对于有符号数在C中并没有明确定义(即使大部分编译器都使用了算数右移)
// 所以即使以下代码可以正常运行,但并不保证没有可移植问题
int logical_right_shift(int x, int k)

    int size = sizeof(int) * 8; // usually sizeof(int) is 4 bytes (32 bits)
    int mask = ~(((0x1 << size) >> k) << 1)
    return (x >> k) & mask;

C++中可以通过模板函数和 union 的结合实现一个通用的逻辑右移运算。

template <typename T, typename P>  
union Converter   
    static_assert(sizeof(T) == sizeof(P), "size not match");  
    T first;  
    P second;  
;

static inline uint32_t Int32ToUInt32(int32_t v)   
    Converter<int32_t, uint32_t> converter;  
    converter.first = v;  
    return converter.second;  
  
  
static inline int32_t UInt32ToInt32(uint32_t v)   
    Converter<int32_t, uint32_t> converter;  
    converter.second = v;  
    return converter.first;  


static inline int32_t logicalRightShift32(int32_t value, uint32_t spaces)   
    return UInt32ToInt32((Int32ToUInt32(value) >> spaces));  


使用移位操作代替乘除运算

参考技术A

"/" 是一个代价很高的操作,使用移位的操作将会更快和更有效

应该改为

但注意的是使用移位应添加注释,因为移位操作不直观,比较难理解。

同样的,对于\'*\'操作,使用移位的操作将会更快和更有效

应该改为

总结:左移代替乘法,右移代替除法。【左乘右除】

以上是关于移位运算的主要内容,如果未能解决你的问题,请参考以下文章

(计算机组成原理)第二章数据的表示和运算-第二节3:定点数的移位运算(算数移位逻辑移位和循环移位)

计算机组成原理——移位运算

计算机组成原理——移位运算

常见的关系运算符(移位运算符)

Java移位运算之算术右移位

移位运算符