什么是按位移位(bit-shift)运算符,它们如何工作?


1380

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

我想知道是,在核心层,什么是位移(<<>>>>>)这样做,它可以帮助什么问题解决了,什么陷阱潜伏在弯曲?换句话说,这是一个绝对的初学者指南,它对所有好处都有好处。


2
在3GL中使用位移的功能性或非功能性情况很少。
Troy DeMonbreun

14
阅读完这些答案后,您可能需要查看以下链接: graphics.stanford.edu/~seander/bithacks.html&jjj.de/bitwizardry/bitwizardrypage.html

1
重要的是要注意,移位对于计算机来说是非常容易和快速的。通过找到在程序中使用位移的方法,可以大大减少内存使用和执行时间。
Hoytman

@Hoytman:但是请注意,好的编译器已经知道了许多技巧,并且通常会更好地识别出什么地方有意义。
塞巴斯蒂安·马赫

Answers:


1712

移位运算符完全按照其名称的含义进行操作。他们移位位。这是对不同移位运算符的简短介绍(或不太简短)。

经营者

  • >> 是算术(或有符号)右移运算符。
  • >>> 是逻辑(或无符号)右移运算符。
  • << 是左移位运算符,并且满足逻辑和算术移位的需求。

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

请注意,这<<<不是运算符,因为它将是多余的。

另请注意,C和C ++不能区分右移运算符。它们仅提供>>运算符,并且右移行为是为有符号类型定义的实现。其余的答案使用C#/ Java运算符。

(在所有主流的C和C ++实现中,包括GCC和Clang / LLVM,>>对带符号的类型都是算术运算。某些代码假定这样做,但这不是标准所保证的。但是,它不是未定义的;该标准要求实现才能对其进行定义。正负号的左移未定义的行为(有符号整数溢出)。因此,除非需要算术右移,否则对无符号类型进行位移位通常是个好主意。)


左移(<<)

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

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

被“移到末尾”的数字丢失。它不会环绕。


逻辑右移(>>>)

逻辑右移与左移相反。与其将位向左移动,不如将它们向右移动。例如,将数字移位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

一旦丢失了位,就无法取回原始值。


算术右移(>>)

算术上的右移与逻辑上的右移完全一样,只是它不是填充零,而是填充最高有效位。这是因为最高有效位是符号位,或区分正数和负数的位。通过填充最高有效位,算术右移将保留符号。

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

10000000 00000000 00000000 01100000

我们有-2,147,483,552。通过算术移位(-2,147,483,552 >> 4)将其右移4个位置将得到:

11111000 00000000 00000000 00000110

或数字-134,217,722。

因此,我们看到通过使用算术右移而不是逻辑右移来保留负数的符号。再一次,我们看到我们正在执行2的幂除法。


303
答案应该更清楚地表明这是特定于Java的答案。C / C ++或C#中没有>>>运算符,并且>>是否传播符号是C / C ++中定义的实现(这是一个潜在的主要难题)
Michael Burr

55
对于C语言,答案是完全错误的。C中没有对“算术”和“逻辑”移位进行有意义的划分。在C中,移位按预期工作在无符号值和正符号值上,它们只是移位位。在负值上,右移是由实现定义的(即,一般情况下不能说什么),并且完全禁止左移-这会产生未定义的行为。
AnT

10
奥黛丽(Audrey),算术和逻辑右移之间肯定存在差异。C只是简单地定义了选择实现。绝对禁止在负值上左移。将0xff000000左移一位,您将得到0xfe000000。
德里克·帕克

16
A good optimizing compiler will substitute shifts for multiplications when possible. 什么?归结到CPU的低级操作时,移位的速度要快几个数量级,一个好的优化编译器会做的恰好相反,即将普通乘法乘以2的幂变成移位。
Mahn 2013年

55
@Mahn,您是出于我的意图向后阅读。将X替换为Y意味着用Y替换X。Y是X的替代。因此,移位是乘法的替代。
德里克·帕克

208

假设我们只有一个字节:

0110110

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

1101100

最左边的零被移出字节,而新的零被附加到字节的右端。

这些位不会翻转;他们被丢弃。这意味着,如果左移1101100,然后右移,您将不会获得相同的结果。

由N个左移相当于乘以2 Ñ

向右移N是(如果使用的是补码),等于除以2 N并四舍五入为零。

如果您使用的是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个周期。

虚拟现实

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位模式下,这两个版本都变得越来越短和越来越快。像Intel Skylake(请参阅http://agner.org/optimize/)这样的现代乱序执行CPU 具有非常快的硬件乘法(低延迟和高吞吐量),因此收益要小得多。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.

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 ++如何全部使用shift + leareturn 320*row + col;

这里要注意的最有趣的事情是x86的移位加法指令(LEA可以执行小左移并同时加法,同时具有add指令性能。ARM甚至更强大:任何指令的一个操作数都可以免费左移或右移。因此,以已知为2的幂的编译时间常数进行缩放甚至比乘法更有效。


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

// Byte1: 11110000
// Byte2: 00001111

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

// value = 000011111110000;

在C ++中,如果将a struct与两个8位成员一起使用,则编译器应为您执行此操作,但实际上它们并不总是如此。


7
在Intel处理器(以及许多其他处理器)上扩展此功能,可以更快地做到这一点:int c,d; c = d << 2; 比这个:c = 4 * d; 有时,甚至“ c = d << 2 + d << 1”也比“ c = 6 * d”快!我在DOS时代将这些技巧广泛用于图形功能,我认为它们不再那么有用了……
Joe Pineda

4
@James:不是,现在是视频卡的固件,其中包含类似的代码,由GPU而不是CPU执行。因此,从理论上讲,您不需要为图形函数实现这样的代码(或像Carmack的black-magic逆根函数一样):-)
Joe Pineda

2
@JoePineda @james编译器作者肯定在使用它们。如果写的c=4*d话,你会得到转变。如果您写的k = (n<0)话也可以通过移位来完成:k = (n>>31)&1避免分支。归根结底,编译器的聪明之处得到了改善,这意味着现在不必在C代码中使用这些技巧,它们会损害可读性和可移植性。如果您正在编写,例如SSE矢量代码,仍然很了解他们。或任何您需要快速使用并且存在编译器未使用的技巧的情况(例如GPU代码)。
greggo 2014年

2
另一个很好的例子:非常常见的事情是if(x >= 1 && x <= 9)可以做到的,因为if( (unsigned)(x-1) <=(unsigned)(9-1)) 将两个条件测试更改为一个可以带来很大的速度优势。特别是当它允许谓词执行而不是分支时。我使用了好几年(有正当理由),直到10年前我注意到ABT编译器已开始在优化器中进行此转换,然后我停了下来。仍然很高兴知道,因为在类似的情况下,编译器无法为您进行转换。或者,如果您正在使用编译器。
greggo 2014年

3
您的“字节”只有7位是有原因的吗?
Mason Watmough '16

103

位操作(包括位移)是低级硬件或嵌入式编程的基础。如果阅读设备规范甚至某些二进制文件格式,则会看到字节,字和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;

说明

为了获得仅绿色值,该值从偏移量5开始到10结束(即6位长),您需要使用(位)掩码,将其应用于整个16位像素时,将产生只有我们感兴趣的部分。

#define GREEN_MASK  0x7E0

适当的掩码是0x7E0,二进制形式是0000011111100000(十进制是2016)。

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

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

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

应用掩码后,您将得到一个16位数字,该数字实际上只是11位数字,因为它的MSB在第11位。Green实际上只有6位长,因此我们需要使用右移(11-6 = 5)来按比例缩小它,因此使用5作为偏移量(#define GREEN_OFFSET 5)。

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

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

1
0x7e0与11111100000相同,十进制为2016。
Saheed

50

位屏蔽和移位

位移通常用于低级图形编程中。例如,给定的像素颜色值编码为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

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


将原始的32bit cl_uint转换为cl_uchar4之类的内容并将其直接作为* .s2来访问不是更容易吗?
David H Parry19年

27

一个陷阱是,以下内容取决于实现(根据ANSI标准):

char x = -1;
x >> 1;

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

实际上,通常是后者。


4
如果我没记错的话,ANSI C标准明确指出这是与实现有关的,因此,如果要在代码上右移带符号的整数,则需要查看编译器的文档以查看其实现方式。
乔·皮内达

是的,我只是想强调ANSI标准本身是这样说的,这不是供应商根本不遵循该标准,也不是该标准没有说明这种特殊情况的情况。
乔·派

22

我只在写提示和技巧。在测试和考试中可能很有用。

  1. n = n*2n = n<<1
  2. n = n/2n = n>>1
  3. 检查n是否为2的幂(1,2,4,8,...): !(n & (n-1))
  4. 获得x位:nn |= (1 << x)
  5. 检查x是偶数还是奇数:(x&1 == 0偶数)
  6. 切换的Ñ x的比特:x ^ (1<<n)

到目前为止,您还必须知道更多吗?
ryyker

@ryyker我添加了一些。我将尝试不断更新它:)
Ravi Prakash

x和n 0索引了吗?
reggaeguitar

广告5:如果是负数怎么办?
Peter Mortensen

因此,我们可以得出二进制2就像十进制10一样的结论吗?移位就像在小数点后的另一个数字后面加上或减去一个数字?
Willy satrio nugroho

8

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

例如:

(long) 4 >> 65

等于2。您可能期望将位右移65次将所有内容归零,但实际上等于:

(long) 4 >> (65 % 64)

对于<<,>>和>>>都是如此。我还没有尝试过其他语言。


嗯,有趣!在C语言中,这是技术上未定义的行为gcc 5.4.0给出警告,但给出25 >> 65;也一样
pizzapant184 '18

2

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))

-3

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

然后,如果您将<<或>>移位31位以上,则结果是无法预期的。通常,将返回原始数字而不是零,这可能是一个非常棘手的错误。

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

更新:在PHP 7 Windows中,PHP构建最终可以使用完整的64位整数:整数 的大小取决于平台,尽管通常的最大值约为20亿(32位带符号)。64位平台的最大值通常约为9E18,但PHP 7之前的Windows始终为32位。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.