Как всегда, это зависит от контекста окружающего кода : например, вы используете x<<1
в качестве индекса массива? Или добавить что-то еще? В любом случае небольшое количество сдвигов (1 или 2) часто может оптимизировать даже больше, чем если бы компилятору пришлось просто сдвигать. Не говоря уже о компромиссе между пропускной способностью, задержкой и узкими местами интерфейса. Выполнение крошечного фрагмента не одномерно.
Инструкции аппаратного сдвига - не единственный вариант компиляции для компиляции x<<1
, но другие ответы в основном предполагают это.
x << 1
в точности эквивалентенx+x
для беззнаковых и для целых чисел со знаком дополнения до 2. Компиляторы всегда знают, на какое оборудование они нацелены во время компиляции, поэтому они могут воспользоваться подобными уловками.
На Intel Haswell , add
имеет 4 за такт пропускной способности , но shl
с немедленным графа имеет только 2 за тактовый пропускную способность . (См. Http://agner.org/optimize/ для таблиц инструкций и других ссылок вx86тег вики). Сдвиги вектора 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
или shl
.) Соглашение о вызовах x86-64 System V передает аргументы в регистры, с первым аргументом edi
и возвращаемым значением eax
, поэтому функция, которая возвращает, x<<10
также заставляет компилятор испускать копирование + сдвиг код.
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. (Семейство Sandybridge и Bulldozer / Ryzen). На Intel это только 1 пропускная способность на такт с задержкой 3с для lea eax, [rdi + rsi + 123]
. (Связано: почему этот код C ++ быстрее, чем моя рукописная сборка для проверки гипотезы Коллатца? Подробно рассматривается.)
В любом случае, для копирования + сдвига на 10 нужна отдельная mov
инструкция. На многих последних процессорах может быть нулевая задержка, но для этого по-прежнему требуется пропускная способность внешнего интерфейса и размер кода. ( Может ли x86 MOV действительно быть «бесплатным»? Почему я вообще не могу воспроизвести его? )
Также по теме: как умножить регистр на 37, используя только 2 последовательные инструкции leal в x86? .
Компилятор также может преобразовывать окружающий код так, чтобы не происходило фактического сдвига, или он сочетался с другими операциями .
Например, if(x<<1) { }
можно использовать and
для проверки всех битов, кроме старшего. На x86 вы должны использовать test
инструкцию, например test eax, 0x7fffffff
/ jz .false
вместо shl eax,1 / jz
. Эта оптимизация работает для любого количества сдвигов, а также работает на машинах, где большие сдвиги выполняются медленно (например, Pentium 4) или отсутствуют (некоторые микроконтроллеры).
Многие ISA имеют инструкции по манипулированию битами, помимо сдвига. например, PowerPC имеет множество инструкций по извлечению / вставке битовых полей. Или ARM имеет сдвиги исходных операндов как часть любой другой инструкции. (Таким образом, инструкции сдвига / поворота - это просто особая форма move
использования смещенного источника.)
Помните, что C не является языком ассемблера . Всегда смотрите на оптимизированный вывод компилятора, когда настраиваете исходный код для эффективной компиляции.