转移)运营商,他们是如何工作的?

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

我想知道的是,在核心层面,什么是位移(<<,>>,>>>),它可以帮助解决什么问题,以及弯道中潜伏着什么? 换句话说,一个绝对的初学者的指南,以完美的位移。


位移操作员完全按照他们的名字暗示。 他们转移位。 以下是对不同班次操作员的简要介绍(或不是简短的介绍)。

运营商

  • >>是算术(或有符号)右移运算符。
  • >>>是逻辑(或无符号)右移运算符。
  • <<是左移运算符,并且满足逻辑和算术移位的需要。
  • 所有这些运算符都可以应用于整数值( intlong ,可能是shortbytechar )。 在某些语言中,将移位运算符应用于小于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