我一直在尝试在业余时间学习C,而其他语言(C#、Java等)都有相同的概念(通常是相同的运算符)。。。

在核心层面,比特移位(<<,>>,>>)做什么,它可以帮助解决什么问题,以及弯道周围潜伏着什么陷阱?换言之,这是一个绝对的初学者指南,可以让你充分利用它的优点。


当前回答

假设我们有一个字节:

0110110

应用一个左移位可以让我们:

1101100

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

比特不会滚动;它们被丢弃。这意味着如果你左移1101100,然后右移它,你不会得到同样的结果。

向左移动N等于乘以2N。

向右移动N(如果你使用的是1的补码)相当于除以2N并舍入到零。

比特移位可以用于极快的乘法和除法,前提是你使用的是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]

12个周期。

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

在32位或64位模式下,这两个版本都变得更短更快。现代无序执行CPU,如Intel Skylake(参见http://agner.org/optimize/)具有非常快的硬件乘法(低延迟和高吞吐量),因此增益要小得多。AMD推土机系列速度稍慢,尤其是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和Microsoft Visual C++在优化返回320*row+col;时如何使用shift+lea;。

这里最值得注意的是,x86有一个移位和加法指令(LEA),它可以同时执行小的左移位和加法,性能与加法指令相同。ARM甚至更强大:任何指令的一个操作数都可以自由左移或右移。因此,通过已知为2次幂的编译时间常数进行缩放甚至比乘法更有效。


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

// Byte1: 11110000
// Byte2: 00001111

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

// value = 000011111110000;

在C++中,如果您使用具有两个8位成员的结构,编译器应该为您做这件事,但实际上并不总是这样。

其他回答

Python中一些有用的位操作/操作。

我用Python实现了Ravi Prakash的答案。

# Basic bit operations
# Integer to binary
print(bin(10))

# Binary to integer
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 second bit is 0

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

请注意,在Java实现中,要移位的位数是根据源代码的大小进行修改的。

例如:

(long) 4 >> 65

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

(long) 4 >> (65 % 64)

这适用于<<、>>和>>。我没有用其他语言试过。

Bitwise运算符用于执行位级操作或以不同方式操作位。发现按位运算速度更快,有时用于提高程序的效率。基本上,按位运算符可以应用于整数类型:long、int、short、char和byte。

按位移位运算符

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

左移位(<<):左移位运算符,将值中的所有位向左移位指定次数。语法:value<<num。这里num指定值左移的位置数。也就是说,<<将指定值中的所有位向左移动num指定的位数。每向左移动一次,高阶位都会被移出(并被忽略/丢失),而在右侧输入一个零。这意味着,当向32位编译器应用左移位时,一旦移位超过位位置31,位就会丢失。如果编译器是64位的,则位位置63之后的位丢失。

输出:6,这里3的二进制表示是0…0011(考虑32位系统),因此当它移位一次时,前导零被忽略/丢失,其余31位都向左移位。最后加零。所以它变成了0…0110,这个数字的十进制表示是6。

如果是负数:

输出:-2,在java负数中,由2的补码表示。SO,-1由2^32-1表示,相当于1….11(考虑32位系统)。当移位一次时,前导位被忽略/丢失,其余31位向左移位,最后加零。所以它变成了,11…10,它的十进制等价物是-2。所以,我认为你对左移及其工作原理有足够的了解。

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

输出: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。

输出:

但记住一点,如果你取y的大值,这个技巧对y的小值很好,它会给你不正确的输出。

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

输出:-5,如上所述,编译器将负值存储为2的补码。因此,-10表示为2^32-10,考虑到32位系统11…0110,以二进制表示。当我们移位/移动一次时,前31个前导位在右侧移位,低阶位丢失/忽略。所以,它变成11…0011,这个数字的十进制表示是-5(我怎么知道数字的符号?因为前导位是1)。有趣的是,如果向右移动-1,结果总是保持-1,因为符号扩展会在高位带来更多的1。

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

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

假设我们有一个字节:

0110110

应用一个左移位可以让我们:

1101100

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

比特不会滚动;它们被丢弃。这意味着如果你左移1101100,然后右移它,你不会得到同样的结果。

向左移动N等于乘以2N。

向右移动N(如果你使用的是1的补码)相当于除以2N并舍入到零。

比特移位可以用于极快的乘法和除法,前提是你使用的是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]

12个周期。

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

在32位或64位模式下,这两个版本都变得更短更快。现代无序执行CPU,如Intel Skylake(参见http://agner.org/optimize/)具有非常快的硬件乘法(低延迟和高吞吐量),因此增益要小得多。AMD推土机系列速度稍慢,尤其是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和Microsoft Visual C++在优化返回320*row+col;时如何使用shift+lea;。

这里最值得注意的是,x86有一个移位和加法指令(LEA),它可以同时执行小的左移位和加法,性能与加法指令相同。ARM甚至更强大:任何指令的一个操作数都可以自由左移或右移。因此,通过已知为2次幂的编译时间常数进行缩放甚至比乘法更有效。


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

// Byte1: 11110000
// Byte2: 00001111

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

// value = 000011111110000;

在C++中,如果您使用具有两个8位成员的结构,编译器应该为您做这件事,但实际上并不总是这样。

位屏蔽和移位

位移位通常用于低级图形编程。例如,以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

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

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

这通常用于编码或解码图像格式,如jpg、png等。