Что такое операторы побитового сдвига (bit-shift) и как они работают? - PullRequest
1297 голосов
/ 26 сентября 2008

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

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

Ответы [ 9 ]

1624 голосов
/ 27 сентября 2008

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

Операторы

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

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

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


Сдвиг влево (<<) </h2> Целые числа хранятся в памяти как последовательность битов. Например, число 6, сохраненное как 32-битное 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

Мы не можем вернуть наше первоначальное значение после потери битов.


Арифметическое смещение вправо (>>)

Арифметическое смещение вправо точно такое же, как и логическое смещение вправо, за исключением того, что вместо заполнения нулями оно дополняется старшим значащим битом. Это связано с тем, что наиболее значимым битом является бит sign , или бит, который различает положительные и отрицательные числа. Заполняя старшим значащим битом, арифметическое смещение вправо сохраняет знак.

Например, если мы интерпретируем эту битовую комбинацию как отрицательное число:

10000000 00000000 00000000 01100000

у нас есть номер -2,147,483,552. Смещение этого вправо на 4 позиции с арифметическим сдвигом (-2 147 483 552 >> 4) даст нам:

11111000 00000000 00000000 00000110

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

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

194 голосов
/ 26 сентября 2008

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

0110110

Применение одного сдвига влево дает нам:

1101100

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

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

Сдвиг влево на N эквивалентен умножению на 2 N .

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

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

Например, в давние времена мы использовали режим 13h (320х200 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

Итого: 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]

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) , которая может делать небольшие сдвиги влево и добавлять одновременно, с производительностью, равной и add инструкция. ARM еще более мощен: один операнд любой инструкции может быть перемещен влево или вправо бесплатно. Таким образом, масштабирование по постоянной времени компиляции, известной как степень 2, может быть даже более эффективным, чем умножение.


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

// Byte1: 11110000
// Byte2: 00001111

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

// value = 000011111110000;

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

96 голосов
/ 27 сентября 2008

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

Простой реальный пример графического программирования - 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;
47 голосов
/ 31 марта 2015

Битовая маскировка и сдвиг

Битовое смещение часто используется в низкоуровневом графическом программировании. Например, заданное значение цвета пикселя, закодированное в 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, ....

27 голосов
/ 27 сентября 2008

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

char x = -1;
x >> 1;

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

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

15 голосов
/ 12 октября 2016

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

  1. n = n*2: n = n<<1
  2. n = n/2: n = n>>1
  3. Проверка, является ли n степенью 2 (1,2,4,8, ...): проверьте !(n & (n-1))
  4. Получение x th бит n: n |= (1 << x)
  5. Проверка, является ли x четным или нечетным: x&1 == 0 (четным)
  6. Переключить n th бит x: x ^ (1<<n)
8 голосов
/ 28 августа 2015

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

Например:

(long) 4 >> 65

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

(long) 4 >> (65 % 64)

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

0 голосов
/ 27 апреля 2019

Некоторые полезные битовые операции / манипуляции в Python. Реализованы ответы @Ravi Prakash в python.

# basic bit operations
# int to bin
print(bin(10))

# bin to int
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 2nd bit is 0

# toggle nth bit of x : x^(1<<n)
# take bin(10)==1010 and toggling 2nd bit in bin(10) we get 1110 === bin(14)
print(10^(1<<2)) 
0 голосов
/ 23 октября 2015

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

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

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

ОБНОВЛЕНИЕ: В PHP7 Windows сборки php наконец-то могут использовать полные 64-битные целые числа: Размер целого числа зависит от платформы, хотя максимальное значение около двух миллиардов является обычным значением (это 32 бита со знаком). Максимальное значение для 64-разрядных платформ обычно составляет около 9E18, за исключением Windows до PHP 7, где оно всегда было 32-разрядным.

...