Що швидше: x << 1 або x << 10?


83

Я не хочу нічого оптимізувати, клянусь, я просто хочу поставити це питання з цікавості. Я знаю, що на більшості апаратних засобів існує команда збірки бітового зсуву (наприклад shl, shr), яка є однією командою. Але чи має значення (наносекундно, або тактово процесор), скільки бітів ви зміщуєте. Іншими словами, чи є щось із наведеного швидше на будь-якому процесорі?

x << 1;

і

x << 10;

І, будь ласка, не ненавидьте мене за це питання. :)


17
Омг, я зазирнув до коду, і моя перша думка - "оператори потокового друку". Мені потрібна перерва.
Кос,

4
Здається, я чую, як хтось ледь чутно каже „передчасна оптимізація”, а може, це лише моя фантазія.
тіа

5
@tia він сказав, що не збирається нічого оптимізувати :)

1
@Grigory так, і тому ми не бачимо, щоб хтось тут пропускав питання з цією фразою. : D
tia

1
Як приклад: нещодавно я визнав, що перехід вліво та зсув вправо не обов'язково витрачають один і той же час процесора. У моєму випадку перехід вправо відбувався набагато повільніше. Спочатку я був здивований, але я думаю, що відповідь полягає в тому, що зсув вліво означає логічний, а зсув вправо, можливо, означає арифметику: stackoverflow.com/questions/141525/…
Крістіан Аммер

Відповіді:


84

Потенційно залежить від процесора.

Однак усі сучасні центральні процесори (x86, ARM) використовують "бочковий перемикач" - апаратний модуль, спеціально розроблений для виконання довільних зсувів у постійний час.

Тож суть ... ні. Без різниці.


21
Чудово, тепер у мене є образ, який говорить моєму центральному процесору, щоб він закрутився в моїй голові ...
Ігнасіо Васкес-Абрамс

11
Помилка - ДУЖЕ багато залежить від процесора. На деяких процесорах це постійний час. На інших це може бути один цикл за зміну (колись я використовував зміну приблизно на 60 000 місць як спосіб ч / б вимірювання тактової частоти процесора). А на інших процесорах можуть існувати лише інструкції щодо однобітових зсувів, у цьому випадку багатобітовий зсув делегується підпрограмі бібліотеки, яка знаходиться в циклі, що ітераціює.
quick_now

4
@quickly_now: Це, безумовно, поганий спосіб вимірювання тактової частоти. Жоден процесор не є досить дурним, щоб насправді робити 60 000 змін; який буде просто перетворено на 60000 mod register_size. Наприклад, 32-розрядний процесор просто використовуватиме 5 найменш значущих бітів рахунку зсувів.
casablanca

4
Перетворювач inmos мав оператор зсуву, який приймав кількість змін - 32-розрядний операнд. Ви можете зробити 4 мільярди змін, якщо хочете, по 1 годиннику кожну. Msgstr "Жоден процесор не є досить дурним". Вибачте - неправильно. Цей зробив. Вам потрібно було кодувати цю частину в асемблері. Компілятори зробили розумну модифікацію / оптимізацію (просто встановіть результат на 0, нічого не робіть).
quick_now

5
На жаль, Pentium 4 втратив перемикач стовбура, що сприяло загальній низькій швидкості інструкцій на такт. Я припускаю, що архітектура Core Blah повернула його назад.
Рассел Борогове

64

Деякі вбудовані процесори мають лише інструкцію "зміна на один". На таких процесорах компілятор змінився б x << 3на ((x << 1) << 1) << 1.

Я думаю, що Motorola MC68HCxx була однією з найпопулярніших сімей з цим обмеженням. На щастя, подібні архітектури зараз є досить рідкісними, більшість із них включають перетворювач стовбурів зі змінним розміром зсуву.

Intel 8051, який має багато сучасних похідних, також не може зміщувати довільну кількість бітів.


12
Все ще поширений на вбудованих мікроконтролерах.
Бен Джексон,

4
Що ви маєте на увазі під "рідкісним"? Відповідно до статистичних даних кількість проданих 8-бітових мікроконтролерів перевищує кількість усіх інших типів MPU.
Вованіум

8-розрядні мікроконтролери мало використовуються для нової розробки, коли ви можете отримати 16-розрядну за ту саму ціну за одиницю (наприклад, MSP430 від TI) з більшою програмою ПЗУ, більшою робочою оперативною пам’яттю та більшими можливостями. І навіть деякі 8-розрядні мікроконтролери мають бочкові перемикачі.
Бен Войгт,

1
Розмір слова мікроконтролера не має нічого спільного з тим, чи має він перемикач стовбурів, сімейство MC68HCxx, про яке я згадав, також має 16-бітові процесори, і всі вони одночасно зміщують лише одну бітну позицію.
Бен Войгт,

Факт того, що більшість 8-бітових мікроконтролерів не мають перемикача стволів, хоча ви маєте рацію, що є такі, для яких це не відповідає дійсності, і є не 8-розрядні без перемикача стволів. Розрядність отримана як надійне наближення для машин із [не] перемикачем стовбура. Також той факт, що ядро ​​центрального процесора для мікроконтролера часто не вибирає модель, але периферія на мікросхемі це робить. І 8-бітові часто вибирають для більш багатих периферійних пристроїв за ту ж ціну.
Вованіум

29

Є багато випадків з цього приводу.

  1. Багато високошвидкісних MPU мають електронну схему, подібну до мультиплексора, що перемикає ствол, яка здійснює будь-який зсув за постійний час.

  2. Якщо MPU має лише 1-бітний зсув x << 10, як правило, це буде повільніше, як це робиться переважно за допомогою 10 змін або копіювання байтів з 2 змінами.

  3. Але відомий поширений випадок, коли це x << 10було б навіть швидше, ніж x << 1. Якщо x - 16 біт, лише нижчі 6 бітів - це обережність (усі інші будуть зміщені), тому MPU потрібно завантажувати лише нижчий байт, таким чином, робити лише один цикл доступу до 8-бітової пам'яті, тоді як x << 10потрібно два цикли доступу. Якщо цикл доступу повільніший за зсув (і очищення нижчого байта), x << 10буде швидшим. Це може стосуватися мікроконтролерів із швидким вбудованим ПЗУ під час доступу до повільної оперативної пам'яті зовнішніх даних.

  4. На додаток до випадку 3, компілятор може піклуватися про кількість значущих бітів x << 10і оптимізувати подальші операції до операцій меншої ширини, наприклад, замінюючи множення 16x16 на 16x8 (оскільки нижчий байт завжди дорівнює нулю).

Зверніть увагу, що деякі мікроконтролери взагалі не мають інструкцій зсуву вліво, вони використовують add x,xзамість цього.


я не розумію, чому x << 10 швидше, ніж x << 8, де в x << 8 потрібно робити навантаження з нижнього байта з 16 бітів, а не робити навантаження і дві зміни. я не розумію.
немає

3
@none: Я не заявив, що x << 10 швидше, ніж x << 8.
Вованіум

9

На ARM це можна зробити як побічний ефект іншої інструкції. Тож потенційно для жодного з них взагалі немає затримки.


1
Чи виконують інструкції однакову кількість циклів? На декількох архітектурах одна і та ж інструкція перетворюється на декілька різних кодів операцій на основі операндів і займає від 1 до 5 циклів.
Nick T

@ Nick Інструкція ARM зазвичай займає від 1 до 2 циклів. Не впевнений у нових архітектурах.
onemasse

2
@ Nick T: Він, говорячи про ARM, має зміну не як спеціальну інструкцію, а як "особливість" багатьох інструкцій з обробки даних. Тобто ADD R0, R1, R2 ASL #3додає R1 і R2, зміщені на 3 біти вліво.
Вованіум


7

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

Майте на увазі, що переміщення будь-чого за межі ширини в бітах даних є "невизначеною поведінкою" в C та C ++. Зсув праворуч підписаних даних також є "визначеним реалізацією". Замість того, щоб надто турбуватися про швидкість, турбуйтеся, що ви отримуєте однакову відповідь на різні реалізації.

Посилання з розділу 3.3.7 ANSI C:

3.3.7 Побітові оператори зсуву

Синтаксис

      shift-expression:
              additive-expression
              shift-expression <<  additive-expression
              shift-expression >>  additive-expression

Обмеження

Кожен з операндів повинен мати інтегральний тип.

Семантика

Цілісні акції проводяться на кожному з операндів. Тип результату - результат підвищеного лівого операнда. Якщо значення правого операнда від’ємне або більше або дорівнює ширині в бітах висунутого лівого операнда, поведінка не визначена.

Результатом E1 << E2 є E1 з лівим зміщенням бітових позицій E2; звільнені біти заповнюються нулями. Якщо E1 має непідписаний тип, значення результату дорівнює E1, помноженому на величину, 2 підняту до рівня E2, зменшене за модулем ULONG_MAX + 1, якщо E1 має тип без підпису long, UINT_MAX + 1 інакше. (Константи ULONG_MAX та UINT_MAX визначені в заголовку.)

Результатом E1 >> E2 є E1, зрушені вправо бітові положення E2. Якщо E1 має непідписаний тип або якщо E1 має підписаний тип і невід'ємне значення, значення результату є невід'ємною частиною частки E1, поділеної на величину, 2, підняту в ступінь E2. Якщо E1 має підписаний тип і від’ємне значення, отримане значення визначається реалізацією.

Тому:

x = y << z;

"<<": y × 2 z ( невизначено, якщо відбувається переповнення);

x = y >> z;

">>": визначено реалізацію для підписаного (найчастіше результат арифметичного зсуву: y / 2 z ).


Я не думаю, що 1u << 100це UB. Це просто 0.
Армен Цирунян

@ Армен Цирунян: Зміщення бітів, 1u << 100як зсув бітів, може бути переповненням; 1u << 100оскільки арифметичний зсув дорівнює 0. За ANSI C <<- це бітовий зсув. en.wikipedia.org/wiki/Arithmetic_shift
вовк

2
@Armen Tsirunyan: Див. Розділ 3.3.7 ANSI - Якщо значення правого операнда від’ємне або більше або дорівнює ширині в бітах висунутого лівого операнда, поведінка не визначена. Отже, вашим прикладом є UB в будь-якій системі ANSI C, якщо немає 101-бітного типу.
вовк

@ carrot-pot: Добре, ти мене переконав :)
Армен Цирунян

Пов’язане: x << (y & 31)все ще може скомпілювати в одну команду зміни без інструкції AND, якщо компілятор знає, що інструкція зміни цільової архітектури маскує рахунок (як це робить x86). (Бажано не кодувати маску жорстким кодом; отримати її від CHAR_BIT * sizeof(x) - 1чогось). Це корисно для написання ідіоми обертання, яка компілюється в одну інструкцію без будь-якого C UB, незалежно від введених даних. ( stackoverflow.com/questions/776508/… ).
Пітер Кордес,

7

Можливо, що на 8-бітному процесорі x<<1насправді може бути набагато повільніше, ніж x<<10для 16-бітового значення.

Наприклад, розумним перекладом x<<1може бути:

byte1 = (byte1 << 1) | (byte2 >> 7)
byte2 = (byte2 << 1)

тоді як x<<10було б простіше:

byte1 = (byte2 << 2)
byte2 = 0

Зверніть увагу, як x<<1зміщення відбувається частіше і навіть далі, ніж x<<10. Крім того, результат x<<10не залежить від вмісту байта1. Це може додатково прискорити роботу.


5

На деяких поколіннях процесорів Intel (P2 або P3? Хоча це не AMD, якщо я добре пам'ятаю), операції з бітовим зміщенням смішно повільні. Зсув на 1 біт завжди повинен бути швидким, хоча він може просто використовувати додавання. Інше питання, яке слід розглянути, полягає в тому, чи швидше зсуви бітів на постійну кількість бітів, ніж зсуви змінної довжини. Навіть якщо операційні коди мають однакову швидкість, на x86 нестійкий правий операнд бітового зсуву повинен займати регістр CL, що накладає додаткові обмеження на розподіл регістрів і може також сповільнити програму.


1
Це Pentium 4. Процесори, отримані з PPro (наприклад, P2 і P3), швидко змінюються. І так, зміна кількості змінних на x86 відбувається повільніше, ніж могла б бути, якщо ви не можете використовувати BMI2 shlx/ shrx/ sarx(Haswell та пізніші версії, та Ryzen). Семантика CISC (прапори незмінені, якщо count = 0) шкодить x86 тут. shl r32, clє 3 uops для сімейства Sandybridge (хоча Intel стверджує, що може скасувати одне з uops, якщо результат позначки не використовується). AMD має одинарний uop shl r32, cl(але повільний подвійний зсув для розширеної точності shld r32, r32, cl)
Пітер Кордес,

1
Зміни (навіть кількість змінних) - це лише одиночне загальне для сімейства P6, але зчитування результату прапора shl r32, clабо безпосереднього відміни від 1 зупиняє фронт-енд, доки зміна не вийде! ( stackoverflow.com/questions/36510095/… ). Компілятори це знають і використовують окрему testінструкцію замість використання прапорця результату зміни. (Але це витрачає вказівки на процесори, де це не проблема, див. Stackoverflow.com/questions/40354978/… )
Пітер Кордес,

3

Як завжди, це залежить від оточуючого контексту коду : наприклад, ви використовуєте x<<1як індекс масиву? Або додати його до чогось іншого? У будь-якому випадку, невелика кількість зсувів (1 або 2) часто може оптимізувати навіть більше, ніж якщо компілятору в кінцевому підсумку доведеться просто перенести. Не кажучи вже про всю пропускну здатність у порівнянні із затримкою та компромісом із вузькими місцями в інтерфейсі. Виконання крихітного фрагмента не є одновимірним.

Інструкції щодо апаратного зсуву не є єдиним варіантом компіляції для компіляції x<<1, але інші відповіді здебільшого припускають, що.


x << 1точно еквівалентноx+x для беззнакових та для доповнених 2 цілих чисел. Під час компіляції компілятори завжди знають, на яке обладнання вони націлені, тому вони можуть скористатися такими трюками.

На Intel Haswell , addмає 4 за такт пропускної здатності , але shlз негайним графа має тільки 2 за тактовий пропускну здатність . (Див. Http://agner.org/optimize/ для таблиць інструкцій та інших посилань утег wiki). Зсуви вектора SIMD складають 1 за такт (2 у Skylake), але цілі числа SIMD для вектора додають 2 за такт (3 у Skylake). Затримка однакова, хоча: 1 цикл.

Існує також спеціальне кодування зсуву за одиницею, shlде підрахунок є неявним у коді дії. 8086 не мав негайних змін підрахунку, лише по одному та за clреєстром. Це в основному актуально для правих змін, тому що ви можете просто додавати для лівих змін, якщо не переміщуєте операнд пам'яті. Але якщо значення потрібно пізніше, краще спочатку завантажити в реєстр. Але в будь-якому випадку, shl eax,1або add eax,eaxна один байт менше shl eax,10, і розмір коду може безпосередньо (декодування / вузькі місця інтерфейсу) або опосередковано (помилки кешу коду L1I) впливати на продуктивність.

Взагалі кажучи, невеликий рахунок зсувів іноді можна оптимізувати в масштабований індекс у режимі адресації на x86. Більшість інших архітектур, які сьогодні широко використовуються, є RISC і не мають режимів адресації з масштабованим індексом, але x86 є досить поширеною архітектурою, щоб про це варто було згадати. (яйце, якщо ви індексуєте масив 4-байтових елементів, є місце для збільшення коефіцієнта масштабу на 1 для int arr[]; arr[x<<1]).


Необхідність копіювання + зсув є типовою в ситуаціях, коли оригінальне значення xвсе ще необхідне. Але більшість цілочисельних інструкцій x86 працюють на місці. (Місце призначення є одним із джерел таких інструкцій, як addor shl.) Конвенція виклику x86-64 System V передає аргументи в регістри, з першим аргументом in ediі повертає значення в eax, тому функція, яка повертає, x<<10також змушує компілятор випускати copy + shift код.

LEAІнструкція дозволяє зрушувати і додавання (з лічильником зрушенням від 0 до 3, оскільки він використовує адресацію режим машини-кодування). Результат поміщається в окремий реєстр.

gcc та clang обидва оптимізують ці функції однаково, як ви можете бачити у досліднику компілятора Godbolt :

int shl1(int x) { return x<<1; }
    lea     eax, [rdi+rdi]   # 1 cycle latency, 1 uop
    ret

int shl2(int x) { return x<<2; }
    lea     eax, [4*rdi]    # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
    ret

int times5(int x) { return x * 5; }
    lea     eax, [rdi + 4*rdi]
    ret

int shl10(int x) { return x<<10; }
    mov     eax, edi         # 1 uop, 0 or 1 cycle latency
    shl     eax, 10          # 1 uop, 1 cycle latency
    ret

LEA з 2 компонентами має затримку в 1 циклі та пропускну здатність 2 за такт на останніх процесорах Intel і AMD. (Сімейство Сендібрідж та бульдозер / Ryzen). У Intel це лише 1 на тактову пропускну здатність із затримкою 3c для lea eax, [rdi + rsi + 123]. (Зв'язаний: Чому цей код C ++ швидше , ніж мій рукописна збірка для перевірки гіпотези Коллатц? Переходить в це в деталях.)

У кожному разі, копіювання + зміщення на 10 потребує окремої movінструкції. Це може бути нульовою затримкою для багатьох останніх процесорів, але вона все одно вимагає інтерфейсу пропускної здатності та розміру коду. ( Чи може MOV x86 справді бути "безкоштовним"? Чому я взагалі не можу його відтворити? )

Також пов’язано: Як помножити регістр на 37, використовуючи лише 2 послідовні інструкції щодо оренди в x86? .


Компілятор також може вільно трансформувати оточуючий код, щоб не було фактичного зрушення або він поєднувався з іншими операціями .

Наприклад, if(x<<1) { }можна використовувати a andдля перевірки всіх бітів, крім старшого біта. На x86 ви б використовували testінструкцію, наприклад test eax, 0x7fffffff/ jz .falseзамість shl eax,1 / jz. Ця оптимізація працює для будь-якого підрахунку змін, а також вона працює на машинах, де зсуви великого рахунку є повільними (наприклад, Pentium 4) або взагалі відсутні (деякі мікроконтролери).

Багато ISA мають інструкції щодо маніпулювання бітами, окрім простого перенесення. наприклад, PowerPC має багато інструкцій із вилучення / вставки бітового поля. Або ARM має зміни вихідних операндів як частина будь-якої іншої інструкції. (Отже, інструкції зсуву / обертання - це лише особлива форма moveвикористання зрушеного джерела.)

Пам’ятайте, C - це не асемблер . Завжди дивіться на оптимізований вихід компілятора, коли ви налаштовуєте свій вихідний код для ефективної компіляції.

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