Як завжди, це залежить від оточуючого контексту коду : наприклад, ви використовуєте x<<1
як індекс масиву? Або додати його до чогось іншого? У будь-якому випадку, невелика кількість зсувів (1 або 2) часто може оптимізувати навіть більше, ніж якщо компілятору в кінцевому підсумку доведеться просто перенести. Не кажучи вже про всю пропускну здатність у порівнянні із затримкою та компромісом із вузькими місцями в інтерфейсі. Виконання крихітного фрагмента не є одновимірним.
Інструкції щодо апаратного зсуву не є єдиним варіантом компіляції для компіляції x<<1
, але інші відповіді здебільшого припускають, що.
x << 1
точно еквівалентноx+x
для беззнакових та для доповнених 2 цілих чисел. Під час компіляції компілятори завжди знають, на яке обладнання вони націлені, тому вони можуть скористатися такими трюками.
На Intel Haswell , add
має 4 за такт пропускної здатності , але shl
з негайним графа має тільки 2 за тактовий пропускну здатність . (Див. Http://agner.org/optimize/ для таблиць інструкцій та інших посилань уx86тег 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 працюють на місці. (Місце призначення є одним із джерел таких інструкцій, як add
or 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 - це не асемблер . Завжди дивіться на оптимізований вихід компілятора, коли ви налаштовуєте свій вихідний код для ефективної компіляції.