什么是按位移位(位移)运算符以及它们如何工作?

时间:2008-09-26 19:47:15

标签: operators bit-manipulation bit-shift binary-operators

我一直在尝试在业余时间学习C语言,其他语言(C#,Java等)具有相同的概念(通常是相同的操作符)......

我想知道的是,在核心层面,位移(<<>>>>>)做了什么,它有什么问题可以解决,以及什么在弯道附近潜伏着什么?换句话说,一个绝对的初学者指导比特移位的所有优点。

11 个答案:

答案 0 :(得分:1623)

比特移位运算符正如其名称所暗示的那样。他们转移位。以下是对不同班次运营商的简要介绍(或不那么简短)。

运营商

  • >>是算术(或签名)右移运营商。
  • >>>是逻辑(或无符号)右移运算符。
  • <<是左移位运算符,可满足逻辑和算术移位的需要。

所有这些运算符都可以应用于整数值(intlong,可能shortbytechar。在某些语言中,将移位运算符应用于任何小于int的数据类型会自动将操作数调整为int

请注意<<<不是运算符,因为它是多余的。另请注意,C和C ++不区分右移运算符。它们仅提供>>运算符,右移行为是为签名类型定义的实现。


左移(&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt; h2&gt;

整数在内存中存储为一系列位。例如,存储为32位int的数字6将是:

00000000 00000000 00000000 00000110

将此位模式移到左侧的一个位置(6 << 1)会产生数字12:

00000000 00000000 00000000 00001100

如您所见,数字向左移动了一个位置,右边的最后一个数字用零填充。您可能还会注意到向左移位相当于乘以2的幂。因此6 << 1相当于6 * 2,而6 << 3相当于6 * 8。一个好的优化编译器会在可能的情况下用乘法替换乘法。

非循环移位

请注意,这些不是循环转换。将此值向左移动一个位置(3,758,096,384 << 1):

11100000 00000000 00000000 00000000

结果为3,221,225,472:

11000000 00000000 00000000 00000000

“失去结束”的数字将丢失。它没有环绕。


逻辑右移(&gt;&gt;&gt;)

逻辑右移与左移相反。它们不是向左移动位,而是向右移动。例如,移动数字12:

00000000 00000000 00000000 00001100

向右移动一个位置(12 >>> 1)将返回原来的6:

00000000 00000000 00000000 00000110

所以我们看到向右移动相当于2的幂除法。

失去的位消失了

但是,班次无法回收“丢失”的位。例如,如果我们改变这种模式:

00111000 00000000 00000000 00000110

到左边4个位置(939,524,102 << 4),我们得到2,147,483,744:

10000000 00000000 00000000 01100000

然后换回((939,524,102 << 4) >>> 4)我们得到134,217,734:

00001000 00000000 00000000 00000110

一旦我们丢失了比特,我们就无法取回原来的价值。


算术右移(&gt;&gt;)

算术右移与逻辑右移非常相似,除了用零填充代替填充,它填充最高有效位。这是因为最重要的位是符号位,或区分正数和负数的位。通过使用最高位填充,算术右移是符号保留。

例如,如果我们将此位模式解释为负数:

10000000 00000000 00000000 01100000
我们的号码是-2,147,483,552。使用算术移位(-2,147,483,552>&gt; 4)将其移至右侧4个位置将为我们提供:

11111000 00000000 00000000 00000110

或数字-134,217,722。

所以我们看到我们通过使用算术右移而不是逻辑右移来保留负数的符号。再一次,我们看到我们正以2的权力进行分裂。

答案 1 :(得分:194)

假设我们有一个字节:

0110110

应用一个左移位器让我们:

1101100

最左边的零移出了字节,并且在字节的右端附加了一个新的零。

这些位不会翻转;他们被丢弃了。这意味着如果你离开1101100然后右移它,你就不会得到相同的结果。

向左移动N相当于乘以2 N

向右移动N(如果使用ones' complement)相当于除以2 N 并舍入为零。

Bitshifting可以用于疯狂快速的乘法和除法,只要你使用2的幂。几乎所有的低级图形例程都使用位移。

例如,回到过去,我们使用模式13h(320x200 256色)进行游戏。在模式13h中,视频存储器按像素顺序布局。这意味着计算像素的位置,您将使用以下数学:

memoryOffset = (row * 320) + column

现在,在那个时代,速度是至关重要的,所以我们会使用位移来进行此操作。

然而,320并不是两个人的力量,所以为了解决这个问题,我们必须找出两个加在一起的力量是什么使得320:

(row * 320) = (row * 256) + (row * 64)

现在我们可以将其转换为左移:

(row * 320) = (row << 8) + (row << 6)

最终结果:

memoryOffset = ((row << 8) + (row << 6)) + column

现在我们获得与以前相同的偏移量,除了代替昂贵的乘法运算,我们使用两个位移......在x86中它会是这样的(注意,自从我完成汇编以来它一直是永远的(编辑器的)注意:纠正了几个错误并添加了一个32位示例)):

mov ax, 320; 2 cycles
mul word [row]; 22 CPU Cycles
mov di,ax; 2 cycles
add di, [column]; 2 cycles
; di = [row]*320 + [column]

; 16-bit addressing mode limitations:
; [di] is a valid addressing mode, but [ax] isn't, otherwise we could skip the last mov

总计:对于任何古老的CPU都有这些时间的28个周期。

VRS

mov ax, [row]; 2 cycles
mov di, ax; 2
shl ax, 6;  2
shl di, 8;  2
add di, ax; 2    (320 = 256+64)
add di, [column]; 2
; di = [row]*(256+64) + [column]

在同一个古老的CPU上进行12次循环。

是的,我们会努力减少16个CPU周期。

在32位或64位模式下,两个版本都变得更短更快。像英特尔Skylake这样的现代无序执行CPU(参见http://agner.org/optimize/)具有非常快的硬件乘法(低延迟和高吞吐量),因此增益要小得多。 AMD Bulldozer系列有点慢,特别是对于64位乘法。在Intel CPU和AMD Ryzen上,两个班次的延迟略低,但指令多于乘法(这可能导致吞吐量降低):

imul edi, [row], 320    ; 3 cycle latency from [row] being ready
add  edi, [column]      ; 1 cycle latency (from [column] and edi being ready).
; edi = [row]*(256+64) + [column],  in 4 cycles from [row] being ready.

VS

mov edi, [row]
shl edi, 6               ; row*64.   1 cycle latency
lea edi, [edi + edi*4]   ; row*(64 + 64*4).  1 cycle latency
add edi, [column]        ; 1 cycle latency from edi and [column] both being ready
; edi = [row]*(256+64) + [column],  in 3 cycles from [row] being ready.

编译器会为您执行此操作:了解gcc, clang, and MSVC all use shift+lea when optimizing return 320*row + col;

的方式

这里要注意的最有趣的事情是x86 has a shift-and-add instruction (LEA)可以执行小的左移并同时添加,其性能为add指令。 ARM更强大:任何指令的一个操作数可以免费左移或右移。因此,通过编译时常量(称为2的幂)进行缩放甚至比乘法更有效。


好的,回到现代......现在更有用的是使用位移来将两个8位值存储在16位整数中。例如,在C#中:

// Byte1: 11110000
// Byte2: 00001111

Int16 value = ((byte)(Byte1 >> 8) | Byte2));

// value = 000011111110000;

在C ++中,如果您使用带有两个8位成员的struct,编译器应该为您执行此操作,但实际上并非总是如此。

答案 2 :(得分:96)

按位运算(包括位移)是低级硬件或嵌入式编程的基础。如果您阅读了设备规范甚至某些二进制文件格式,您将看到字节,字和dword,分为非字节对齐的位域,其中包含各种感兴趣的值。访问这些位字段以进行读/写是最常见的用法。

图形编程中一个简单的实例是16位像素表示如下:

  bit | 15| 14| 13| 12| 11| 10| 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1  | 0 |
      |       Blue        |         Green         |       Red          |

要获得绿色值,您可以这样做:

 #define GREEN_MASK  0x7E0
 #define GREEN_OFFSET  5

 // Read green
 uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;

<强>解释

为了获得绿色ONLY的值,从偏移5开始到10结束(即6位长),你需要使用(位)掩码,当应用于整个16位像素时,只会产生我们感兴趣的位。

#define GREEN_MASK  0x7E0

相应的掩码为0x7E0,二进制为0000011111100000(以十进制表示2016年)。

uint16_t green = (pixel & GREEN_MASK) ...;

要应用遮罩,请使用AND运算符(&amp;)。

uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;

应用掩码之后,最终会得到一个16位数,这个数字实际上只是一个11位数,因为它的MSB位于第11位。绿色实际上只有6位长,所以我们需要使用右移(11 - 6 = 5)来缩小它,因此使用5作为偏移(#define GREEN_OFFSET 5)。

同样常见的是使用位移进行快速乘法和除以2的幂:

 i <<= x;  // i *= 2^x;
 i >>= y;  // i /= 2^y;

答案 3 :(得分:47)

Bit Masking&amp;移

位移通常用于低级图形编程。例如,以32位字编码的给定像素颜色值。

 Pixel-Color Value in Hex:    B9B9B900
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

为了更好地理解,标有什么部分的相同二进制值代表什么颜色部分。

                                 Red     Green     Blue       Alpha
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

假设我们想要获得此像素颜色的绿色值。我们可以通过屏蔽转移轻松获得该值。

我们的面具:

                  Red      Green      Blue      Alpha
 color :        10111001  10111001  10111001  00000000
 green_mask  :  00000000  11111111  00000000  00000000

 masked_color = color & green_mask

 masked_color:  00000000  10111001  00000000  00000000

逻辑&运算符确保仅保留掩码为1的值。我们现在要做的最后一件事是通过将所有这些位向右移动16位(逻辑右移)来获得正确的整数值。

 green_value = masked_color >>> 16

Etvoilá,我们有一个整数表示像素颜色中的绿色数量:

 Pixels-Green Value in Hex:     000000B9
 Pixels-Green Value in Binary:  00000000 00000000 00000000 10111001 
 Pixels-Green Value in Decimal: 185

这通常用于编码或解码jpgpng...等图像格式。

答案 4 :(得分:27)

一个问题是以下是依赖于实现的(根据ANSI标准):

char x = -1;
x >> 1;

x现在可以是127(01111111)或仍然是-1(11111111)。

在实践中,通常是后者。

答案 5 :(得分:15)

我只是在撰写技巧和窍门,可能会在测试/考试中发挥作用。

  1. n = n*2n = n<<1
  2. n = n/2n = n>>1
  3. 检查n是2的幂(1,2,4,8,...):检查!(n & (n-1))
  4. 获取n x th 位:n |= (1 << x)
  5. 检查x是偶数还是奇数:x&1 == 0(偶数)
  6. 切换x的{em> n th 位:x ^ (1<<n)

答案 6 :(得分:8)

请注意,在Java实现中,要移位的位数是源的大小。

例如:

(long) 4 >> 65

等于2.您可能希望将位向右移动65次会将所有内容归零,但它实际上相当于:

(long) 4 >> (65 % 64)

适用于&lt;&lt;,&gt;&gt;和&gt;&gt;&gt;。我没有用其他语言试过。

答案 7 :(得分:3)

按位运算符用于按位执行操作或以不同方式操纵位。发现按位运算要快得多,并且有时用于提高程序效率。 基本上,按位运算符可以应用于整数类型: int 字符字节

按位移位运算符

它们分为左移和右移两类。

  • 左移(<<):左移运算符,将值中的所有位向左移指定的次数。语法:值<< num。此处num指定将值左移值的位置数。也就是说,<< <<将指定值中的所有位向左移动num指定的位数。对于左移,高位移出(并忽略/丢失),而右移零。这意味着当将左移应用于32位编译器时,一旦将它们移过位位置31,就会丢失这些位。如果编译器是64位的,则在位位置63之后将丢失位。

enter image description here

输出:6 ,此处3的二进制表示为0 ... 0011(考虑32位系统),因此当它移位一次时,前导零将被忽略/丢失,其余所有零31位左移。并在末尾添加零。因此它变为0 ... 0110,该数字的十进制表示形式为6。

  • 如果是负数:

Code for Negative number.

输出:-2 ,在Java负数中,由2的补码表示。 SO,-1用2 ^ 32-1表示,等效于1 .... 11(考虑32位系统)。当移位一次时,前导位将被忽略/丢失,其余的31位将向左移位,最后一个加零。因此它变为11 ... 10,其十进制等效值为-2。 因此,我认为您已经掌握了有关左移及其工作原理的足够知识。

  • 右移(>>)::右移运算符,将值中的所有位向右移指定的次数。语法:value >> num,num指定将值右移value的位置数。也就是说,>>将所有指定位中的所有位移动/移位num所指定的位数。 以下代码片段将值35右移两个位置:

enter image description here

输出:8 ,因为32位系统中35的二进制表示为00 ... 00100011,所以当我们将其右移两次时,前30个前导位将被移动/移位在右边,两个低位丢失/忽略,并且在前导位添加两个零。因此,它变为00 .... 00001000,此二进制表示形式的十进制等效值为8。 或者有一个简单的数学技巧来找出以下代码的输出:概括地说,我们可以说x >> y = floor(x / pow(2,y))。考虑上面的示例,x = 35和y = 2,所以35/2 ^ 2 = 8.75,如果我们取下限值,则答案为8。

enter image description here

输出:

enter image description here

但是请记住一件事,如果您将y的值取大,则该技巧对y的值就很合适。

  • 如果为负数: 由于为负数,因此右移运算符以有符号和无符号两种模式工作。在带符号的右移运算符(>>)中,如果是正数,则用0填充前导位。如果是负数,则用1填充前导位。以保留符号。这称为“符号扩展”。

enter image description here

输出:-5 ,如上所述,编译器将负值存储为2的补码。因此,考虑到32位系统11 .... 0110,-10表示为2 ^ 32-10,并且采用二进制表示。当我们移位/移动一次时,前31个前导位在右侧移位,而低阶位丢失/被忽略。因此,它变为11 ... 0011,该数字的十进制表示形式是-5(我怎么知道数字的符号?因为前导位是1)。 有趣的是,如果将-1右移,结果始终保持为-1,因为符号扩展会不断在高位中引入更多的数字。

  • 无符号右移(>>>):该运算符还将位向右移动。有符号和无符号之间的区别是,如果数字为负,则后者用1填充前导位,而在任何一种情况下前者用零填充。现在出现了一个问题,如果通过带符号的右移运算符获得所需的输出,为什么我们需要无符号的右运算。通过一个示例来理解这一点,如果要移动的内容不代表数字值,则可能不希望出现符号扩展。当您使用基于像素的值和图形时,这种情况很常见。在这些情况下,无论初始值是什么,您通常都希望将零移位到高阶位。

enter image description here

输出:2147483647 ,因为在32位系统中-2表示为11 ... 10。当我们将一位移位一位时,前31个前导位向右移动/移位,而低阶位则丢失/被忽略,并且将零添加到前导位。因此,它变为011 ... 1111(2 ^ 31-1),其十进制等效值为2147483647。

答案 8 :(得分:1)

按位移位运算符可移动二进制对象的位值。左操作数指定要移位的值。右边的操作数指定值中要移位的位数。结果不是左值。这两个操作数具有相同的优先级,并且是从左到右的关联。

Operator     Usage

 <<           Indicates the bits are to be shifted to the left.

 >>           Indicates the bits are to be shifted to the right.

每个操作数必须具有整数或枚举类型。编译器对操作数执行整数提升,然后将正确的操作数转换为int类型。结果与左操作数的类型相同(在算术转换之后)。

右操作数不应具有负值或大于或等于要移位的表达式的位宽度的值。这些值按位移位的结果是不可预测的。

如果右操作数的值为0,则结果为左操作数的值(在通常的算术转换之后)。

<<运算符用零填充空位。例如,如果left_op的值为4019,则left_op的位模式(16位格式)为:

0000111110110011

表达式left_op << 3产生:

0111110110011000

表达式left_op >> 3产生:

0000000111110110

答案 9 :(得分:0)

Python中一些有用的位操作/操纵。在python中实现了@Ravi Prakash答案。

# basic bit operations
# int to bin
print(bin(10))

# bin to int
print(int('1010',2))

# multiplying x with 2 .... x**2== x << 1
print(200<<1)

# dividing x with 2 .... x /2 == x >> 1
print(200>>1)

# modulo x with 2 .... x%2 == x&1
if 20&1==0:
    print("20 is a even number")

# check if n is power of 2 : check !(n & (n-1))
print(not(33 &(33-1)))

# getting xth bit of n : (n>>x)&1
print((10>>2)&1) # bin of 10==1010 and 2nd bit is 0

# toggle nth bit of x : x^(1<<n)
# take bin(10)==1010 and toggling 2nd bit in bin(10) we get 1110 === bin(14)
print(10^(1<<2)) 

答案 10 :(得分:-3)

请注意,Windows平台上只提供32位版本的PHP。

然后,如果你转移&lt;&lt;或&gt;&gt;超过31位,结果是不可想象的。通常会返回原始数字而不是零,这可能是一个非常棘手的错误。

当然如果你使用64位版本的PHP(unix),你应该避免移位超过63位。但是,例如,MySQL使用64位BIGINT,因此不应存在任何兼容性问题。

更新:从PHP7 Windows开始,php构建终于能够使用完整的64位整数: 整数的大小取决于平台,但最大值约为20亿是通常的值(32位有符号)。 64位平台的最大值通常约为9E18,除了在PHP之前的Windows上,它总是32位。