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