Что быстрее: x << 1 или x << 10?

Я не хочу ничего оптимизировать, клянусь, я просто хочу задать этот вопрос из любопытства. Я знаю, что на большинстве аппаратных средств имеется команда сборки бит-сдвига (например, shl, shr), которая является единственной командой. Но имеет ли значение (наносекундный, или процессорный такт), сколько бит вы сдвигаете. Другими словами, любой из них быстрее работает на любом CPU?

x << 1;

и

x << 10;

И, пожалуйста, не ненавидите меня за этот вопрос.:)

Ответ 1

Потенциально зависит от CPU.

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

Итак, нижняя строка... нет. Нет разницы.

Ответ 2

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

Я думаю, что Motorola MC68HCxx была одной из самых популярных семей с этим ограничением. К счастью, такие архитектуры сейчас довольно редки, большинство из них теперь включают в себя баррель-сдвиг с переменным размером сдвига.

Intel 8051, который имеет множество современных производных, также не может сдвигать произвольное количество бит.

Ответ 3

На этом много случаев.

  • Многие высокоскоростные MPU имеют баррель-сдвиг, мультиплексорную электронную схему, которая делает любой сдвиг в постоянное время.

  • Если MPU имеет только 1 бит сдвиг x << 10, как правило, будет медленнее, поскольку он в основном выполняется с помощью 10 сдвигов или байтов с двумя сменами.

  • Но есть известный общий случай, когда x << 10 будет даже быстрее, чем x << 1. Если x - 16 бит, только младшие 6 бит - это уход (все остальные будут сдвинуты), поэтому MPU необходимо загрузить только младший байт, таким образом, сделать только один цикл доступа к 8-разрядной памяти, тогда как x << 10 нужны два циклы доступа. Если цикл доступа медленнее, чем сдвиг (и чистый младший байт), x << 10 будет быстрее. Это может относиться к микроконтроллерам с быстрым бортовым программным ПЗУ при доступе к медленной внешней памяти данных.

  • В дополнение к случаю 3 компилятор может заботиться о количестве значимых бит в x << 10 и оптимизировать дальнейшие операции для более низких значений, например, заменять умножение 16x16 на 16x8 (так как младший байт всегда равен нулю).

Обратите внимание: некоторые микроконтроллеры вообще не имеют сдвиговой-левую инструкцию, вместо этого они используют add x,x.

Ответ 4

В ARM это может быть сделано как побочный эффект другой команды. Поэтому, возможно, нет никакой задержки для любого из них.

Ответ 6

Это зависит как от процессора, так и от компилятора. Даже если базовый процессор имеет произвольный сдвиг бит с помощью переключателя баррелей, это произойдет, только если компилятор воспользуется этим ресурсом.

Имейте в виду, что перемещение чего-либо за пределами ширины в битах данных "undefined поведение" в C и С++. Правый сдвиг подписанных данных также определяется "реализацией". Вместо того, чтобы слишком сильно беспокоиться о скорости, будьте обеспокоены тем, что вы получаете одинаковый ответ на разные реализации.

Цитата из раздела ANSI C 3.3.7:

3.3.7 Операторы побитового сдвига

Синтаксис

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

Ограничения

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

Семантика

Интегральные акции выполняемых на каждом из операндов. Тип результата - это результат продвинутый левый операнд. Если значение правильного операнда отрицательный или больше или равно ширине в бит продвинутого левого операнда, поведение undefined.

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

Результат E1 → E2 равен E1 сдвинутые вправо позиции E2. Если E1 имеет неподписанный тип, или если E1 имеет подписанный тип и неотрицательное значение, значение результата интегральная часть частного E1 делится на количество, 2 поднято до мощность E2. Если E1 имеет подписанный тип и отрицательное значение, итоговое значение от реализации.

Итак:

x = y << z;

"<": y × 2 z ( undefined, если происходит переполнение);

x = y >> z;

" → ": реализация определена для подписанного (чаще всего результат арифметического сдвига: y/2 z).

Ответ 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. Это может ускорить операцию дополнительно.

Ответ 8

В некоторых поколениях процессоров Intel (P2 или P3? Not AMD, хотя, если я правильно помню), операции с битрейтом смехотворно медленны. Bitshift на 1 бит всегда должен быть быстрым, поскольку он может просто использовать добавление. Другой вопрос, который следует рассмотреть, заключается в том, что бит-бит по постоянному количеству бит быстрее, чем сдвиги переменной длины. Даже если коды операций имеют одинаковую скорость, на x86 непостоянный правый операнд бит-чарта должен занимать регистр CL, что накладывает дополнительные ограничения на распределение регистров и может замедлить работу программы тоже.

Ответ 9

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

Команды аппаратного смены не являются компилятором только для компиляции x<<1, но другие ответы в основном предполагают, что.


x << 1 в точности эквивалентен x+x для unsigned и для 2-значных целых чисел. Компиляторы всегда знают, на каком оборудовании они нацелены во время компиляции, поэтому они могут воспользоваться такими трюками.

Вкл Intel Haswell, add имеет пропускную способность 4 на каждый такт, но shl с немедленным подсчетом имеет только 2 на каждую пропускную способность, (См. http://agner.org/optimize/ для таблиц инструкций и других ссылок в tag 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 или shl.). Соглашение о вызове System V x86-64 передает аргументы в регистры, с первым аргументом arg в 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 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 с двумя компонентами имеет 1 задержку цикла и пропускную способность 2-на-часы на последних процессорах Intel и AMD. (Семья Сэндибридж и бульдозер/Рызень). На Intel это только 1 за пропускную способность каждого такта с задержкой 3 с для lea eax, [rdi + rsi + 123]. (Связано: Почему этот код на С++ быстрее, чем моя рукописная сборка для тестирования гипотезы Collatz? подробно рассматривается в этой статье.)

Во всяком случае, для копирования + сдвига на 10 требуется отдельная команда mov. Это может быть нулевая латентность для многих последних процессоров, но она по-прежнему требует ширины полосы пропускания и размера кода переднего плана. (Может ли MOV x86 действительно "бесплатно" ? Почему я не могу воспроизвести это вообще?

Также связано: Как умножить регистр на 37, используя только две последовательные инструкции по управлению в x86?.


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

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

Многие ISA имеют инструкции по манипулированию битами, за исключением просто сдвига. например PowerPC имеет множество инструкций по извлечению/вставке битового поля. Или ARM имеет сдвиги исходных операндов как часть любой другой команды. (Так что команды shift/rotate являются только специальной формой move, используя сдвинутый источник.)

Помните, что C не является языком ассемблера. Всегда смотрите оптимизированный вывод компилятора, когда вы правильно настраиваете исходный код.