Що таке оператори бітового зсуву (біт-зсуву) і як вони працюють?


1382

Я намагався вивчити С у вільний час, а інші мови (C #, Java тощо) мають те саме поняття (і часто одні й ті ж оператори) ...

Що мені цікаво, на рівні ядра, що робить біт зсуву ( <<, >>, >>>) робити, які проблеми вона може допомогти вирішити, і які підводні камені підстерігають навколо вигину? Іншими словами, абсолютний посібник для початківців щодо зміщення бітів у всій його доброті.


2
Функціональних або нефункціональних випадків, коли ви б використовували бітшифтинг в 3GL, небагато.
Troy DeMonbreun

15
Прочитавши ці відповіді, ви можете переглянути ці посилання: graphics.stanford.edu/~seander/bithacks.html & jjj.de/bitwizardry/bitwizardrypage.html
кігті

1
Важливо зауважити, що переміщення бітів надзвичайно просто та швидко робиться для комп'ютерів. Знайшовши способи використання перемикання бітів у вашій програмі, ви можете значно скоротити використання пам'яті та час виконання.
Хойтман

@Hoytman: Але зауважте, що хороші компілятори вже знають багато цих хитрощів і зазвичай краще розпізнають, де це має сенс.
Себастьян Мах

Відповіді:


1713

Оператори, що змінюють біт, роблять саме те, що випливає з назви. Вони зміщують шматочки. Ось короткий (або не дуже короткий) вступ до різних операторів зміни.

Оператори

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

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

Зауважте, що <<<це не оператор, оскільки це було б зайвим.

Також зауважте, що C і C ++ не розрізняють операторів правого зсуву . Вони забезпечують лише >>оператора, а поведінка, що зміщує право, - це реалізація, визначена для підписаних типів. Решту відповіді використовують оператори C # / Java.

(У всіх основних реалізаціях C і C ++, включаючи GCC і Clang / LLVM, >>на підписаних типах є арифметикою. Деякі коди передбачають це, але це не щось, що стандартні гарантії. Хоча це не визначено , однак, стандарт вимагає реалізації, щоб визначити його одним так чи інакше. Однак, зсув лівих від’ємних підписаних чисел - це невизначена поведінка (переливання підписаного цілого числа). Тому, якщо вам не потрібен арифметичний правий зсув, зазвичай, це гарна ідея зробити біт-перемикання з непідписаними типами.)


Зсув вліво (<<)

Цілі числа зберігаються в пам'яті у вигляді серії біт. Наприклад, число 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

результати в 2121252572:

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.


304
Відповідь має чіткіше зрозуміти, що це відповідь, специфічна для Java. У C / C ++ або C # немає оператора >>>, і чи >> поширює цей знак ознака, це реалізація, визначена в C / C ++ (головний потенційний готч)
Майкл Берр

56
Відповідь є абсолютно невірною в контексті мови С. Немає значущого поділу на "арифметичні" та "логічні" зрушення в C. В C зрушення працюють, як очікувалося, на непідписані значення та на позитивні підписані значення - вони просто зміщують біти. Що стосується негативних значень, правильна зміна визначається реалізацією (тобто нічого не можна сказати про те, що вона робить взагалі), а зсув вліво просто заборонено - це виробляє невизначену поведінку.
ANT

10
Одрі, безумовно, є різниця між арифметичним і логічним правильним зміщенням. C просто залишає вибір вибір визначеним. І зсув ліворуч на негативні значення точно не заборонено. Зсуньте 0xff000000 вліво і ви отримаєте 0xfe000000.
Дерек Парк

16
A good optimizing compiler will substitute shifts for multiplications when possible. Що? Бітчіфти - це на порядок швидше, коли мова йде про операції з низьким рівнем процесора, хороший оптимізуючий компілятор зробив би точно навпаки, тобто перетворив звичайні множення на потужності двох у бітові зсуви.
Ман

55
@Mahn, ти читаєш це назад з мого наміру. Заміна Y на X означає заміну X на Y. Y є заміною для X. Отже, зсув є замінником множення.
Парк Дерека

209

Скажімо, у нас є один байт:

0110110

Застосовуючи єдиний лівий біт-зсув, ми отримуємо:

1101100

Найменший лівий нуль був зміщений з байта, а новий нуль був доданий до правого кінця байта.

Біти не перекидаються; їх відкидають. Це означає, що якщо ви зсунете 1101100 вліво, а потім змініть його праворуч, ви не отримаєте того самого результату.

Зрушення вліво на N еквівалентно множенню на 2 N .

Зсув праворуч на 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

Всього: 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.

vs.

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 + 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 ++ компілятори повинні робити це для вас, якщо ви використовували a structз двома 8-бітними членами, але на практиці вони не завжди.


8
Розширюючи це, на процесорах Intel (і багатьох інших) це зробити швидше: int c, d; c = d << 2; Чим це: c = 4 * d; Іноді навіть "c = d << 2 + d << 1" швидше, ніж "c = 6 * d" !! Ці трюки я широко використовував для графічних функцій в епоху DOS, я не думаю, що вони вже так корисні ...
Джо Пінеда,

5
@James: не зовсім, в даний час це швидше прошивка відеокарти, яка включає в себе такий код, щоб виконувати GPU, а не процесор. Тож теоретично вам не потрібно реалізовувати такий код (або як чорна магія зворотного кореневого функції Кармака) для графічних функцій :-)
Джо Пінеда

3
@JoePineda @james Автори-компілятори, безумовно, використовують їх. Якщо ви напишете, c=4*dви отримаєте зміну. Якщо ви пишете, k = (n<0)це може бути зроблено і зі змінами: k = (n>>31)&1щоб уникнути гілки. Підсумок, це поліпшення кмітливості компіляторів означає, що зараз не потрібно використовувати ці хитрощі в коді C, і вони ставлять під загрозу читабельність та портативність. Ще дуже добре їх знати, якщо ви пишете, наприклад, векторний код SSE; або будь-яка ситуація, коли вам це потрібно швидко, і є хитрість, яку компілятор не використовує (наприклад, код GPU).
greggo

2
Ще один хороший приклад: дуже поширене, if(x >= 1 && x <= 9)що можна зробити, оскільки if( (unsigned)(x-1) <=(unsigned)(9-1)) зміна двох умовних тестів на одну може бути великою перевагою швидкості; особливо, коли це дозволяє передбачити виконання замість гілок. Я використовував це протягом років (де це виправдано), поки 10 років тому я не помітив, що компілятори почали робити це перетворення в оптимізаторі, тоді я зупинився. Ще добре знати, оскільки є подібні ситуації, коли компілятор не може зробити перетворення за вас. Або якщо ви працюєте над компілятором.
greggo

3
Чи є причина, що ваш "байт" становить лише 7 біт?
Мейсон Ватмаф

104

Бітові операції, включаючи бітовий зсув, є основоположними для апаратного забезпечення або вбудованого програмування. Якщо ви прочитаєте специфікацію для пристрою або навіть деяких форматів бінарних файлів, ви побачите байти, слова та 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;

1
0x7e0 - це те саме, що 11111100000, що становить 2016 у десятковій кількості.
Сахед

50

Бітова маскування та зсув

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


Чи не простіше видати оригінал, скажімо, 32-бітний cl_uint, як щось на зразок cl_uchar4 та отримати доступ до байти, який ви хочете безпосередньо, як * .s2?
Девід Н Паррі

27

Один з причин - це те, що від реалізації залежить (згідно стандарту ANSI):

char x = -1;
x >> 1;

x тепер може бути 127 (01111111) або ще -1 (11111111).

На практиці зазвичай це останнє.


4
Якщо я пам'ятаю це правильно, стандарт ANSI C прямо говорить про те, що це залежить від реалізації, тому вам потрібно перевірити документацію компілятора, щоб побачити, як він реалізований, якщо ви хочете праворуч змінити цілі цілі підписані у вашому коді.
Джо Пінеда

Так, я просто хотів наголосити на тому, що сам стандарт ANSI говорить так, це не той випадок, коли продавці просто не дотримуються стандарту, або що стандарт нічого не говорить про цей випадок.
Джо Пінеда,

22

Я пишу лише поради та підказки. Це може бути корисно в тестах та іспитах.

  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- го біта n:n |= (1 << x)
  5. Перевірка, чи х парне чи непарне: x&1 == 0(парне)
  6. Перемістіть n- й біт x:x ^ (1<<n)

Повинно бути ще кілька, що ви знаєте на даний момент?
ryyker

@ryyker Я додав ще кілька. Я спробую продовжувати його оновлювати :)
Раві Пракаш

Чи індексуються х і n 0?
reggaeguitar

Оголошення 5 .: Що робити, якщо це від’ємне число?
Пітер Мортенсен

Отже, чи можемо ми зробити висновок, що 2 у двійковій формі - це як 10 у десятковій? і бітове зміщення - це як додавання або віднімання ще одного числа за іншим числом у десятковій формі?
Willy satrio nugroho

8

Зауважте, що в реалізації Java кількість бітів для зсуву залежить від розміру джерела.

Наприклад:

(long) 4 >> 65

дорівнює 2. Можливо, ви очікуєте, що зміщення бітів вправо 65 разів зведе нанівець усе, але це фактично еквівалент:

(long) 4 >> (65 % 64)

Це справедливо для <<, >> та >>>. Я не пробував це іншими мовами.


Ага, цікаво! У С це технічно невизначена поведінка . gcc 5.4.0дає попередження, але дає 2за 5 >> 65; так само.
pizzapants184

2

Деякі корисні бітові операції / маніпуляції в Python.

Я реалізував відповідь Раві Пракаша в Python.

# 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))

-3

Пам’ятайте, що лише 32-бітна версія PHP доступна на платформі Windows.

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

Звичайно, якщо ви використовуєте 64-бітну версію PHP (Unix), вам слід уникати переміщення на більш ніж 63 біти. Однак, наприклад, MySQL використовує 64-розрядний BIGINT, тому проблем із сумісністю не повинно виникнути.

ОНОВЛЕННЯ: З PHP 7 Windows, збірки PHP, нарешті, можуть використовувати повні 64-бітові цілі числа: розмір цілого числа залежить від платформи, хоча максимальне значення близько двох мільярдів - це звичайне значення (це 32 біти підписано). 64-розрядні платформи зазвичай мають максимальне значення приблизно 9E18, за винятком Windows у програмі до PHP 7, де вона завжди була 32-бітовою.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.