转移)运营商,他们是如何工作的?
我一直在尝试在业余时间学习C语言,其他语言(C#,Java等)也有相同的概念(并且通常是相同的操作符)...
我想知道的是,在核心层面,什么是位移(<<,>>,>>>),它可以帮助解决什么问题,以及弯道中潜伏着什么? 换句话说,一个绝对的初学者的指南,以完美的位移。
位移操作员完全按照他们的名字暗示。 他们转移位。 以下是对不同班次操作员的简要介绍(或不是简短的介绍)。
运营商
>>
是算术(或有符号)右移运算符。 >>>
是逻辑(或无符号)右移运算符。 <<
是左移运算符,并且满足逻辑和算术移位的需要。 所有这些运算符都可以应用于整数值( int
, long
,可能是short
和byte
或char
)。 在某些语言中,将移位运算符应用于小于int
任何数据类型会自动将操作数调整为int
。
请注意, <<<
不是运算符,因为它是多余的。 还要注意C和C ++不区分右移运算符。 它们只提供>>
运算符,右移行为是为签名类型定义的实现。
左移(<<)
整数作为一系列位存储在内存中。 例如,存储为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
变得“结束”的数字丢失了。 它不会环绕。
逻辑右移(>>>)
逻辑右移是对左移的反向。 他们只是向右移动,而不是将位移到左侧。 例如,将数字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)会给我们:
11111000 00000000 00000000 00000110
或数字-134,217,722。
所以我们看到,通过使用算术右移,而不是逻辑右移,我们保留了负数的符号。 再一次,我们看到我们正在进行权力分割。
假设我们有一个字节:
0110110
应用单个左移位让我们:
1101100
最左边的零被移出该字节,并且在该字节的右端追加了一个新的零。
位不翻转; 他们被丢弃。 这意味着如果您左移1101100,然后右移,则不会得到相同的结果。
向左移N等同于乘以2N。
向右移动N是(如果您使用的是补码)等于2N除和舍入为零。
如果您正在使用2的幂,则移位可用于疯狂的快速乘法和除法。几乎所有的低级图形例程都使用偏移。
例如,在过去的时代,我们使用模式13h(320x200 256色)来进行游戏。 在模式13h中,视频存储器按照每个像素顺序排列。 这意味着要计算一个像素的位置,你可以使用下面的数学公式:
memoryOffset = (row * 320) + column
现在回到那个时代,速度非常关键,所以我们会使用bitshift来做这个操作。
然而,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位模式下,两个版本都变得更短更快。 像Intel Skylake这样的现代无序执行CPU(见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.
与
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和MSVC在优化return 320*row + col;
时如何使用shift + lea return 320*row + col;
。
这里最值得注意的是x86有一个移位和加法指令( LEA
),它可以执行较小的左移并同时添加,并具有性能和add
指令。 ARM功能更加强大:任何指令的一个操作数可以左右移位。 因此,通过一个已知为2的幂的编译时常量进行缩放可能比乘法更有效。
好吧,回到现代......现在更有用的东西是使用位移来将两个8位值存储在一个16位整数中。 例如,在C#中:
// Byte1: 11110000
// Byte2: 00001111
Int16 value = ((byte)(Byte1 >> 8) | Byte2));
// value = 000011111110000;
在C ++中,编译器应该为你做这个,如果你使用的struct
有两个8位成员,但在实践中并不总是如此。
按位操作(包括位移)是低级硬件或嵌入式编程的基础。 如果你阅读一个设备的规范甚至是一些二进制文件格式,你会看到字节,单词和双字,分成非字节对齐的位域,其中包含各种感兴趣的值。 访问这些位域以进行读/写是最常用的用法。
图形编程中一个简单的实例是一个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位长)的绿色ONLY值,您需要使用(位)掩码,该掩码在应用于整个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位。 绿色实际上只有6位长,所以我们需要使用右移(11 - 6 = 5)来缩放它,因此使用5作为偏移量( #define GREEN_OFFSET 5
)。
通常使用比特移位进行快速乘法和2的幂除法:
i <<= x; // i *= 2^x;
i >>= y; // i /= 2^y;
链接地址: http://www.djcxy.com/p/203.html
上一篇: shift) operators and how do they work?
下一篇: How to manage a redirect request after a jQuery Ajax call