Я не хочу ничего оптимизировать, клянусь, я просто хочу задать этот вопрос из любопытства. Я знаю, что на большинстве оборудования есть сборочная команда битового сдвига (например shl
, shr
), которая представляет собой единственную команду. Но имеет ли значение (с точки зрения наносекунды или с точки зрения процессора), сколько бит вы сдвигаете. Другими словами, может ли один из перечисленных ниже вариантов работать быстрее на любом процессоре?
x << 1;
и
x << 10;
И, пожалуйста, не ненавидьте меня за этот вопрос. :)
Ответы:
Потенциально зависит от процессора.
Однако все современные процессоры (x86, ARM) используют «баррель-шифтер» - аппаратный модуль, специально разработанный для выполнения произвольных сдвигов за постоянное время.
Итак, суть в том ... нет. Нет разницы.
источник
60000 mod register_size
. Например, 32-разрядный процессор будет использовать только 5 младших битов счетчика сдвига.Некоторые встроенные процессоры имеют только команду «сдвиг на один». На таких процессорах компилятор изменится
x << 3
на((x << 1) << 1) << 1
.Я думаю, что Motorola MC68HCxx была одним из самых популярных семейств с этим ограничением. К счастью, такие архитектуры сейчас довольно редки, в большинстве из них теперь есть баррель-шифтер с переменным размером сдвига.
Intel 8051, у которого есть много современных производных, также не может сдвигать произвольное количество бит.
источник
На этот счет много случаев.
Многие высокоскоростные MPU имеют баррель-шифтер, электронную схему, подобную мультиплексору, которая выполняет любое переключение за постоянное время.
Если MPU имеет только 1 бит, сдвиг
x << 10
обычно будет медленнее, как это обычно делается с помощью 10 смен или байтового копирования с 2 сменами.Но известен распространенный случай, когда
x << 10
было бы даже быстрее, чемx << 1
. Если x равен 16 бит, заботятся только младшие 6 бит (все остальные будут сдвинуты), поэтому MPU должен загружать только младший байт, таким образом, сделать только один цикл доступа к 8-битной памяти, в то время какx << 10
требуется два цикла доступа. Если цикл доступа медленнее, чем сдвиг (и очищает младший байт),x << 10
будет быстрее. Это может относиться к микроконтроллерам с быстрым встроенным программным ПЗУ при доступе к медленной внешней памяти данных.В дополнение к случаю 3, компилятор может заботиться о количестве значимых битов
x << 10
и оптимизировать дальнейшие операции для операций с меньшей шириной, например, замену умножения 16x16 на единицу 16x8 (поскольку младший байт всегда равен нулю).Обратите внимание, некоторые микроконтроллеры вообще не имеют инструкции сдвига влево, они ее используют
add x,x
.источник
В ARM это можно сделать как побочный эффект другой инструкции. Таким образом, потенциально для любого из них нет никакой задержки.
источник
ADD R0, R1, R2 ASL #3
складывает R1 и сдвигает R2 на 3 бита влево.Вот мой любимый процессор , на котором он работает
x<<2
вдвое дольшеx<<1
:)источник
Это зависит как от процессора, так и от компилятора. Даже если базовый ЦП имеет произвольный битовый сдвиг с помощью сдвигателя, это произойдет только в том случае, если компилятор воспользуется этим ресурсом.
Имейте в виду, что сдвиг чего-либо за пределы ширины в битах данных является «неопределенным поведением» в C и C ++. Правый сдвиг подписанных данных также «определяется реализацией». Вместо того, чтобы слишком беспокоиться о скорости, позаботьтесь о том, чтобы вы получили одинаковый ответ в разных реализациях.
Цитата из раздела 3.3.7 ANSI C:
Так:
x = y << z;
«<<»: y × 2 z ( не определено, если происходит переполнение);
x = y >> z;
">>": определено реализацией для подписи (чаще всего результат арифметического сдвига: y / 2 z ).
источник
1u << 100
это УБ. Это всего лишь 0.1u << 100
как битовый сдвиг может быть переполнением;1u << 100
поскольку арифметический сдвиг равен 0. Согласно ANSI C,<<
это битовый сдвиг. en.wikipedia.org/wiki/Arithmetic_shiftx << (y & 31)
все еще может компилироваться в одну инструкцию сдвига без инструкции И, если компилятор знает, что инструкция сдвига целевой архитектуры маскирует счетчик (как это делает x86). (Желательно не кодировать маску жестко; получить ееCHAR_BIT * sizeof(x) - 1
или что-то в этом роде.) Это полезно для написания идиомы поворота, которая компилируется в одну инструкцию без C UB независимо от входных данных. ( stackoverflow.com/questions/776508/… ).Вполне возможно, что на 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. Это может дополнительно ускорить операцию.источник
На некоторых поколениях процессоров Intel (P2 или P3? Не AMD, если я правильно помню) операции сдвига битов до смешного медленные. Битовый сдвиг на 1 бит всегда должен быть быстрым, поскольку он может просто использовать сложение. Другой вопрос, который следует рассмотреть, заключается в том, являются ли сдвиги на постоянное количество бит быстрее, чем сдвиги переменной длины. Даже если коды операций имеют одинаковую скорость, на x86 непостоянный правый операнд битового сдвига должен занимать регистр CL, что накладывает дополнительные ограничения на выделение регистров и может замедлять работу программы.
источник
shlx
/shrx
/sarx
(Haswell и более поздние версии , и Ryzen). Семантика CISC (флаги не изменяются, если count = 0) вредит x86 здесь.shl r32, cl
составляет 3 мопа на семействе Sandybridge (хотя Intel утверждает, что может отменить один из мопов, если результат флага не используется). AMD имеет одинарную опциюshl r32, cl
(но медленную двойную смену для повышенной точностиshld r32, r32, cl
),shl r32, cl
или с немедленным, отличным от 1, останавливает интерфейс до тех пор, пока смена не прекратится ! ( stackoverflow.com/questions/36510095/… ). Компиляторы знают это и используют отдельнуюtest
инструкцию вместо использования флага результата сдвига. (Но это тратит впустую инструкции для процессоров, где это не проблема, см. Stackoverflow.com/questions/40354978/… )Как всегда, это зависит от контекста окружающего кода : например, вы используете
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 не является языком ассемблера . Всегда смотрите на оптимизированный вывод компилятора, когда настраиваете исходный код для эффективной компиляции.
источник