我一直在尝试业余时间学习C,其他语言(C#,Java等)具有相同的概念(并且通常具有相同的运算符)...
我想知道是,在核心层,什么是位移(<<
,>>
,>>>
)这样做,它可以帮助什么问题解决了,什么陷阱潜伏在弯曲?换句话说,这是一个绝对的初学者指南,它对所有好处都有好处。
我一直在尝试业余时间学习C,其他语言(C#,Java等)具有相同的概念(并且通常具有相同的运算符)...
我想知道是,在核心层,什么是位移(<<
,>>
,>>>
)这样做,它可以帮助什么问题解决了,什么陷阱潜伏在弯曲?换句话说,这是一个绝对的初学者指南,它对所有好处都有好处。
Answers:
移位运算符完全按照其名称的含义进行操作。他们移位位。这是对不同移位运算符的简短介绍(或不太简短)。
>>
是算术(或有符号)右移运算符。>>>
是逻辑(或无符号)右移运算符。<<
是左移位运算符,并且满足逻辑和算术移位的需求。所有这些操作符可以应用到整数值(int
,long
,可能short
和byte
或char
)。在某些语言中,将移位运算符应用于小于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的幂除法。
A good optimizing compiler will substitute shifts for multiplications when possible.
什么?归结到CPU的低级操作时,移位的速度要快几个数量级,一个好的优化编译器会做的恰好相反,即将普通乘法乘以2的幂变成移位。
假设我们只有一个字节:
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位成员一起使用,则编译器应为您执行此操作,但实际上它们并不总是如此。
c=4*d
话,你会得到转变。如果您写的k = (n<0)
话也可以通过移位来完成:k = (n>>31)&1
避免分支。归根结底,编译器的聪明之处得到了改善,这意味着现在不必在C代码中使用这些技巧,它们会损害可读性和可移植性。如果您正在编写,例如SSE矢量代码,仍然很了解他们。或任何您需要快速使用并且存在编译器未使用的技巧的情况(例如GPU代码)。
if(x >= 1 && x <= 9)
可以做到的,因为if( (unsigned)(x-1) <=(unsigned)(9-1))
将两个条件测试更改为一个可以带来很大的速度优势。特别是当它允许谓词执行而不是分支时。我使用了好几年(有正当理由),直到10年前我注意到ABT编译器已开始在优化器中进行此转换,然后我停了下来。仍然很高兴知道,因为在类似的情况下,编译器无法为您进行转换。或者,如果您正在使用编译器。
位操作(包括位移)是低级硬件或嵌入式编程的基础。如果阅读设备规范甚至某些二进制文件格式,则会看到字节,字和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;
位移通常用于低级图形编程中。例如,给定的像素颜色值编码为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
等。
我只在写提示和技巧。在测试和考试中可能很有用。
n = n*2
: n = n<<1
n = n/2
: n = n>>1
!(n & (n-1))
n
n |= (1 << x)
x&1 == 0
偶数)x ^ (1<<n)
请注意,在Java实现中,要移位的位数由源的大小修改。
例如:
(long) 4 >> 65
等于2。您可能期望将位右移65次将所有内容归零,但实际上等于:
(long) 4 >> (65 % 64)
对于<<,>>和>>>都是如此。我还没有尝试过其他语言。
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))