Что такое операторы побитового сдвига (bit-shift) и как они работают?

Я пытался выучить C в свободное время и другие языки (C#, Java и т. д.) имеют одинаковую концепцию (и часто одни и те же операторы) ...

Мне интересно, на уровне ядра, что делает бит-сдвиг (>, >>>) какие проблемы он может помочь решить, и какие gotchas скрываются за поворотом? Другими словами, абсолютное руководство для начинающих по бит-сдвигу во всей его доброте.

8 ответов


операторы сдвига битов делают именно то, что подразумевает их имя. Они меняют биты. Вот краткое (или не очень краткое) введение в различные операторы сдвига.

Операторы

  • >> - арифметический (или подписанный) оператор сдвига вправо.
  • >>> является логическим (или беззнаковым) оператором сдвига вправо.
  • << левый оператор переноса, и отвечает потребностямы и логического и арифметического сдвиги.

все эти операторы могут быть применены к целочисленным значениям (int, long, возможно short и byte или char). В некоторых языках применение операторов shift к любому типу данных меньше int автоматически изменяет размер операнда на int.

обратите внимание, что <<< не является оператором, потому что это было бы избыточно. Также обратите внимание, что C и C++ не различают операторы сдвига вправо. Они обеспечивают только >> оператор, а поведение сдвига вправо определяется реализацией для подписанных типов.


сдвиг влево (

целые числа хранятся в памяти в виде ряда битов. Например, число 6 хранится как 32-битное int будет:

00000000 00000000 00000000 00000110

смещение этого битового шаблона влево на одну позицию (6 << 1) приведет к числу 12:

00000000 00000000 00000000 00001100

как вы можете видеть, цифры сдвинулись влево на один позиция, и последняя цифра справа заполняется нулем. Можно также отметить, что сдвиг влево эквивалентен умножению на степени числа 2. Так что 6 << 1 эквивалентно 6 * 2 и 6 << 3 эквивалентно 6 * 8. Хороший оптимизирующий компилятор заменит умножения сдвигами, когда это возможно.

non-круговой сдвиг

Пожалуйста, обратите внимание, что это не циклические сдвиги. Смещение этого значения влево на одну позицию (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. Смещение этого вправо на 4 позиции с арифметическим сдвигом (-2,147,483,552 > > 4) даст нам:

11111000 00000000 00000000 00000110

или число -134,217,722.

Итак, мы видим, что мы сохранили знак отрицательных чисел, используя арифметический сдвиг вправо, а не логический сдвиг вправо. И однажды опять же, мы видим, что мы выполняем разделение по степеням 2.


Допустим, у нас есть один байт:

0110110

применение одного левого bitshift получает нас:

1101100

крайний левый ноль был смещен из байта, и новый ноль был добавлен к правому концу байта.

биты не опрокидываются; они отбрасываются. Это означает, что если вы левый сдвиг 1101100, а затем правый сдвиг, вы не получите тот же результат.

сдвиг влево на N равносильно умножению на 2N.

сдвиг вправо на N (если вы используете одни дополняют) эквивалентно делению на 2N и округление до нуля.

Bitshifting можно использовать для безумно быстрого умножения и деления, при условии, что вы работаете с мощностью 2. Почти все низкоуровневые графические процедуры используют bitshifting.

например, в старые времена мы использовали режим 13h (320x200 256 цветов) для игр. В Режим 13h, видеопамять была выложена последовательно на пиксель. Это означало, что для вычисления местоположения пикселя вы будете использовать следующую математику:

memoryOffset = (row * 320) + column

теперь, в те дни, скорость была критической, поэтому мы будем использовать bitshifts для выполнения этой операции.

однако 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

Итого: 28 циклов на любом древнем CPU были эти синхронизации.

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 циклов процессора.

в 32 или 64-битном режиме обе версии становятся намного короче и быстрее. Современные процессоры выполнения вне заказа, такие как Intel Skylake (см.http://agner.org/optimize/) имеют очень быстрое аппаратное умножение (низкая латентность и высокая пропускная способность), поэтому коэффициент усиления намного меньше. AMD Bulldozer-семейство немного медленнее, особенно для 64-битного умножения. На процессорах Intel и 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 используют shift+lea при оптимизации return 320*row + col;.

самое интересное, чтобы отметить здесь, что x86 имеет инструкцию shift-and-add (LEA) это может делать небольшие левые сдвиги и добавлять одновременно, с производительностью as и add инструкция. ARM еще более мощный: один операнд любой инструкции может быть смещен влево или вправо бесплатно. Таким образом, масштабирование константой времени компиляции, которая известна как мощность-2, может быть еще более эффективным, чем умножение.


хорошо, в наши дни... что-то более полезное теперь было бы использовать bitshifting для хранения двух 8-битных значений в 16-битном целочисленном. Например, в C#:

// Byte1: 11110000
// Byte2: 00001111

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

// value = 000011111110000;

в C++, компиляторы должны сделать это для вас, если вы использовали struct С двумя 8-битными членами, но на практике не всегда.


побитовые операции, включая битовый сдвиг, имеют основополагающее значение для низкоуровневого оборудования или встроенного программирования. Если Вы читаете спецификацию для устройства или даже некоторые двоичные форматы файлов, вы увидите байты, слова и dwords, разбитые на не-байтовые выровненные битовые поля, которые содержат различные значения интереса. Доступ к этим битовым полям для чтения / записи является наиболее распространенным использованием.

простой реальный пример в графическом программировании заключается в том, что 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-битном. Зеленый на самом деле имеет длину всего 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

Et voilá, у нас есть целое число, представляющее количество зеленого в цвете пикселей:

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

это часто используется для кодирования или декодирования форматов изображений, таких как jpg,png,....


один gotcha заключается в том, что следующее зависит от реализации (в соответствии со стандартом ANSI):

char x = -1;
x >> 1;

x теперь может быть 127 (01111111) или все еще -1 (11111111).

на практике, это обычно последний.


обратите внимание, что в реализации Java количество битов для сдвига модифицируется размером источника.

например:

(long) 4 >> 65

равна 2. Вы можете ожидать, что смещение битов вправо 65 раз обнулит все, но на самом деле это эквивалент:

(long) 4 >> (65 % 64)

Это верно для >, и >>>. Я не пробовал его на других языках.


Я пишу только советы и рекомендации, может быть полезным в тестах/экзаменах.

  1. n = n*2: n = n<<1
  2. n = n/2: n = n>>1
  3. проверка, если n-мощность 2 (1,2,4,8,...): check !(n & (n-1))
  4. начало xth немного n: n |= (1 << x)
  5. проверка четности или нечетности x:x&1 == 0 (даже)
  6. включить nth бит x: x ^ (1<<n)

имейте в виду, что только 32-разрядная версия PHP доступна на платформе Windows.

тогда, если вы, например, сдвинете > более чем на 31 бит, результаты будут неожиданными. Обычно возвращается исходное число вместо нулей, и это может быть действительно сложная ошибка.

конечно, если вы используете 64-битную версию PHP (unix), вы должны избегать смещения более чем на 63 бита. Однако, например, MySQL использует 64-битный BIGINT, поэтому не должно быть никаких проблема совместимости.

UPDATE: из окон PHP7 сборки php, наконец, могут использовать полные 64-битные целые числа: размер integer зависит от платформы, хотя максимальное значение около двух миллиардов привычное значение (это 32-битное знаковое). 64-разрядные платформы обычно имеют максимальное значение около 9E18, за исключением Windows до PHP 7, где он всегда был 32 бит.