Сколько однобайтовых NOP может выполнить Skylake за один цикл

Я выравниваю цели ветвления с NOP, и иногда CPU выполняет эти NOP, до 15 NOP. Сколько однобайтовых NOP может выполнить Skylake за один цикл? А как насчет других Intel-совместимых процессоров, таких как AMD? Я интересуюсь не только Скайлэйком, но и другими микроархитами. Сколько циклов может потребоваться для выполнения последовательности из 15 NOP? Я хочу знать, стоит ли дополнительный размер кода и дополнительное время выполнения добавления этих NOPs своей ценой. Это не я, кто добавляет эти NOP, а ассемблер автоматически всякий раз, когда я пишу align директивы.

Обновление: мне удалось автоматически вставить многобайтовый NOPs.

3 ответа

Решение

Как правило, Skylake может выполнить четыре однобайтовых nops за один цикл. Это было верно, по крайней мере, в отношении микроархитектуры Sandy Bridge (далее SnB).

Skylake и другие, вернувшиеся в SnB, также, как правило, смогут выполнять четыре байта длиннее одного nop также в одном цикле, если только они не настолько длинны, чтобы столкнуться с внешними ограничениями.


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

Это не я, кто добавляет эти NOP, а ассемблер. Он довольно тупой и не поддерживает параметры (BASM) для выравнивания - есть только один вариант - размер границы.

Я не знаю, что такое "BASM", и я не могу найти какую-либо ссылку на него в Интернете (кроме этого, который, очевидно, не является x86), но если он не поддерживает многобайтовые NOP, вам действительно нужен другой ассемблер. Это просто базовый материал, который был в руководствах по архитектуре Intel и AMD в течение многих лет. Ассемблер Gnu может сделать это для директив ALIGN, как и Microsoft MASM. Ассемблеры NASM и YASM с открытым исходным кодом также поддерживают это, и любой из них может быть легко интегрирован в любую существующую систему сборки.

Под многобайтовыми NOP я имею в виду следующее, которое вы можете найти в руководствах по процессорам AMD и Intel:

Length   |  Mnemonic                                 |  Opcode Bytes
---------|-------------------------------------------|-------------------------------------
1 byte   |  NOP                                      |  90
2 bytes  |  66 NOP                                   |  66 90
3 bytes  |  NOP DWORD [EAX]                          |  0F 1F 00
4 bytes  |  NOP DWORD [EAX + 00H]                    |  0F 1F 40 00
5 bytes  |  NOP DWORD [EAX + EAX*1 + 00H]            |  0F 1F 44 00 00
6 bytes  |  66 NOP DWORD [EAX + EAX*1 + 00H]         |  66 0F 1F 44 00 00
7 bytes  |  NOP DWORD [EAX + 00000000H]              |  0F 1F 80 00 00 00 00
8 bytes  |  NOP DWORD [EAX + EAX*1 + 00000000H]      |  0F 1F 84 00 00 00 00 00
9 bytes  |  66 NOP DWORD [EAX + EAX*1 + 00000000H]   |  66 0F 1F 84 00 00 00 00 00

Рекомендации по последовательности, предложенные двумя производителями, слегка расходятся после 9 байтов, но такие длинные NOP... не очень распространены. И, вероятно, это не имеет большого значения, так как чрезвычайно длинные инструкции NOP с чрезмерным количеством префиксов в любом случае приведут к снижению производительности. Они работают вплоть до Pentium Pro, поэтому их сегодня поддерживают повсеместно.

Agner Fog может сказать следующее о многобайтовых NOP:

Многобайтовая инструкция NOP имеет код операции 0F 1F + фиктивный операнд памяти. Длина многобайтовой инструкции NOP может быть отрегулирована путем необязательного добавления 1 или 4 байтов смещения и байта SIB к фиктивному операнду памяти и путем добавления одного или нескольких 66H префиксы. Чрезмерное количество префиксов может вызвать задержку на старых микропроцессорах, но по крайней мере два префикса приемлемы для большинства процессоров. Таким образом, NOP любой длины до 10 байтов могут быть созданы не более чем с двумя префиксами. Если процессор может обрабатывать несколько префиксов без штрафа, длина может составлять до 15 байтов.

Все избыточные / лишние префиксы просто игнорируются. Преимущество, конечно, в том, что многие новые процессоры имеют более низкие скорости декодирования для многобайтовых NOP, что делает их более эффективными. Они будут быстрее чем серия 1-байтовых NOP (0x90) инструкции.

Возможно, даже лучше, чем многобайтовые NOP для выравнивания, использовать более длинные формы инструкций, которые вы уже используете в своем коде. Эти более длинные кодировки не занимают больше времени (они влияют только на пропускную способность декодирования), поэтому они быстрее / дешевле, чем NOP. Примеры этого:

  • Использование байт-форм mod-reg-r /m инструкций, таких как INC, DEC, PUSH, POP и т. д. вместо коротких версий
  • Используя эквивалентную инструкцию, которая длиннее, например ADD вместо INC или же LEA вместо MOV,
  • Кодирование более длинных форм непосредственных операндов (например, 32-разрядные непосредственные значения вместо 8-разрядных немедленных расширений со знаком)
  • Добавление байтов SIB и / или ненужных префиксов (например, размер операнда, сегмент и REX в длинном режиме)

В руководствах Агнера Фога подробно рассказывается и приводятся примеры этих методов.

Я не знаю ни одного ассемблера, который будет выполнять эти преобразования / оптимизации автоматически (ассемблеры выбирают самую короткую версию по понятным причинам), но обычно они имеют строгий режим, в котором вы можете принудительно использовать определенную кодировку, или вы можно просто вручную выдать байты инструкции. В любом случае вы делаете это только в высокочувствительном коде, где работа действительно окупится, что существенно ограничивает объем требуемых усилий.

Я хочу знать, стоит ли дополнительный размер кода и дополнительное время выполнения добавления этих NOPs своей ценой.

В общем нет. Хотя выравнивание данных является чрезвычайно важным и практически свободным (несмотря на размер двоичного файла), выравнивание кода намного менее важно. Существуют случаи в замкнутых циклах, когда это может иметь существенное значение, но это имеет значение только в горячих точках в вашем коде, которые ваш профилировщик уже определит, и затем вы можете выполнить манипуляции для ручного выравнивания кода при необходимости. Иначе я бы не волновался об этом.

Имеет смысл выравнивать функции, так как байты заполнения между ними никогда не выполняются (вместо того, чтобы использовать здесь NOP, вы часто будете видеть INT 3 или неверная инструкция, например UD2), но я бы не стал выравнивать все ваши цели веток внутри функций просто как само собой разумеющееся. Делайте это только в известных критических внутренних циклах.

Как всегда, Агнер Фог говорит об этом и говорит это лучше, чем я мог:

Большинство микропроцессоров извлекают код в выровненных 16-байтовых или 32-байтовых блоках. Если важная запись подпрограммы или метка перехода оказываются ближе к концу 16-байтового блока, микропроцессор получит только несколько полезных байтов кода при извлечении этого блока кода. Возможно, ему также придется извлечь следующие 16 байтов, прежде чем он сможет декодировать первые инструкции после метки. Этого можно избежать, выровняв важные записи подпрограммы и записи цикла по 16. Выравнивание по 8 гарантирует, что по крайней мере 8 байтов кода могут быть загружены с первой выборкой команды, что может быть достаточно, если инструкции маленькие. Мы можем выровнять записи подпрограммы по размеру строки кэша (обычно 64 байта), если подпрограмма является частью критической горячей точки и предыдущий код вряд ли будет выполнен в том же контексте.

Недостатком выравнивания кода является то, что некоторое пространство кеша теряется на пустые места перед выровненными записями кода.

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

Выравнивание записи подпрограммы так же просто, как и NOP Это необходимо перед входом в подпрограмму, чтобы адрес делился на 8, 16, 32 или 64, по желанию. Ассемблер делает это с ALIGN директивы. NOP То, что вставлено, не замедлит производительность, потому что они никогда не выполняются.

Выровнять запись цикла более проблематично, поскольку предыдущий код также выполняется. Может потребоваться до 15 NOP чтобы выровнять запись цикла по 16. Эти NOP будет выполнено до ввода цикла, и это будет стоить процессорного времени. Более эффективно использовать более длинные инструкции, которые ничего не делают, чем использовать много однобайтовых NOP "S. Лучшие современные ассемблеры будут делать именно это и использовать такие инструкции, как MOV EAX,EAX а также LEA EBX,[EBX+00000000H] заполнить пространство перед ALIGN nn заявление. LEA инструкция особенно гибкая. Можно дать инструкцию как LEA EBX,[EBX] любой длины от 2 до 8 путем различного добавления байта SIB, префикса сегмента и смещения в один или четыре байта нуля. Не используйте двухбайтовое смещение в 32-битном режиме, так как это замедлит декодирование. И не используйте более одного префикса, потому что это замедлит декодирование на старых процессорах Intel.

Использование псевдо-NOP, таких как MOV RAX,RAX а также LEA RBX,[RBX+0] недостатком наполнителей является то, что он ложно зависит от регистра и использует ресурсы исполнения. Лучше использовать многобайтовую инструкцию NOP, которую можно настроить на желаемую длину. Многобайтовая инструкция NOP доступна во всех процессорах, которые поддерживают инструкции условного перемещения, то есть Intel PPro, P2, AMD Athlon, K7 и более поздние версии.

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

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

Как ни странно, я тестировал код, который несколько раз пытался оптимизировать, и не нашел большого преимущества в выравнивании цели ветвления цикла. Например, я писал оптимизированный strlen функция (библиотеки Gnu есть одна, но у Microsoft нет), и попытался выровнять цель основного внутреннего цикла на 8-байтовой, 16-байтовой и 32-байтовой границах. Ничто из этого не имело большого значения, особенно если сравнивать с другими резкими скачками производительности, которые я делал при переписывании кода.

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

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

Раньше выравнивание было немного большим делом в оригинальной микроархитектуре Core 2 (Penryn и Nehalem), где существенные узкие места декодирования означали, что, несмотря на ширину выпуска в 4 раза, у вас были трудные времена, когда его исполнительные блоки были заняты. С введением µop кеша в Sandy Bridge (одна из немногих приятных функций Pentium 4, которая в конечном итоге была вновь введена в расширенное семейство P6), пропускная способность внешнего интерфейса была значительно увеличена, и это стало намного меньше проблема.

Честно говоря, компиляторы тоже не очень хорошо справляются с оптимизацией такого типа. -O2 Переключатель для GCC подразумевает -falign-functions, -falign-jumps, -falign-loops, а также -falign-labels переключатели с предпочтением по умолчанию для выравнивания по 8-байтовым границам. Это довольно тупой подход, и пробег меняется. Как я упоминал выше, отчеты различаются о том, может ли отключение этого выравнивания и использование компактного кода на самом деле повысить производительность. Более того, самое лучшее, что вы увидите в компиляторе - это вставка многобайтовых NOP. Я не видел ни одного, который бы использовал более длинные формы инструкций или радикально переставил код для выравнивания. Так что нам еще предстоит пройти долгий путь, и это очень сложная проблема для решения. Некоторые люди работают над этим, но это просто показывает, насколько трудной на самом деле является проблема: "Небольшие изменения в потоке команд, такие как вставка одной инструкции NOP, могут привести к значительным отклонениям производительности с эффектом разоблачения". усилия по компиляции и оптимизации производительности для восприятия нежелательной случайности ". (Обратите внимание, что, хотя интересно, что бумага родом из первых двух дней Core, которая больше всего пострадала от штрафов за несоосность, как я упоминал ранее. Я не уверен, что вы увидите такие же кардинальные улучшения в современных микроархитектурах, но Я не могу сказать наверняка в любом случае, потому что я не запустил тест. Может быть, Google наймет меня, и я смогу опубликовать еще одну статью?)

Сколько однобайтовых NOP может выполнить Skylake за один цикл? А как насчет других Intel-совместимых процессоров, таких как AMD? Я интересуюсь не только Скайлэйком, но и другими микроархитами. Сколько циклов может потребоваться для выполнения последовательности из 15 NOP?

На такие вопросы можно ответить, посмотрев таблицы инструкций Agner Fog и выполнив поиск NOP, Я не стану извлекать все его данные из этого ответа.

В целом, тем не менее, просто знайте, что NOP не являются бесплатными. Хотя им не требуется исполнительный блок / порт, они все равно должны проходить через конвейер, как и любая другая инструкция, и поэтому они в конечном итоге находятся в узком месте из-за проблем (и / или выхода из строя) процессора. Обычно это означает, что вы можете выполнять от 3 до 5 операций NOP за такт.

NOP также по-прежнему занимают место в кэш-памяти, что означает снижение плотности кода и эффективности кеша.

Во многих отношениях вы можете думать о NOP как эквивалент XOR reg, reg или же MOV который попадает в интерфейс из-за переименования регистра.

См. Также ответ Коди о многих хороших вещах, которые я опускаю, потому что он уже рассказал об этом.


Никогда не используйте несколько однобайтовых NOP. У всех ассемблеров есть способы получить длинные NOP; увидеть ниже.

15 NOP требуют 3,75 с для выдачи с обычными 4 за такт, но могут вообще не замедлять ваш код, если он был узким местом в длинной цепочке зависимостей в этот момент. Они занимают место в ROB до самого выхода на пенсию. Единственное, что они не делают, это используют порт исполнения. Дело в том, что производительность процессора не является аддитивной. Вы не можете просто сказать: "это займет 5 циклов, и это займет 3, так что вместе они займут 8". Смысл выполнения не по порядку состоит в том, чтобы перекрываться с окружающим кодом.

Наихудшее влияние многих однобайтовых коротких NOP на семейство SnB состоит в том, что они имеют тенденцию переполнять ограничение uop-кэша в 3 строки на выровненный фрагмент 32B кода x86. Это будет означать, что весь блок 32B всегда должен выполняться от декодеров, а не кэш-памяти uop или буфера цикла. (Буфер цикла работает только для циклов, которые имеют все свои мопы в кеше мопов).

Вы должны когда-либо иметь не более 2 NOP подряд, которые действительно выполняются, и только в том случае, если вам нужно заполнить более чем 10B или 15B или что-то еще. (Некоторые процессоры работают очень плохо при декодировании инструкций с очень большим количеством префиксов, поэтому для NOP, которые фактически выполняются, вероятно, лучше не повторять префиксы до 15B (максимальная длина инструкции x86).


YASM по умолчанию делает длинные NOP. Для NASM используйте smartalign стандартный пакет макросов, который по умолчанию не включен. Это заставляет вас выбирать стратегию NOP.

%use smartalign
ALIGNMODE p6, 32     ;  p6 NOP strategy, and jump over the NOPs only if they're 32B or larger.

ИДК, если 32 является оптимальным. Также помните, что самые длинные NOP могут использовать множество префиксов и медленно декодироваться в Silvermont или AMD. Проверьте руководство NASM для других режимов.

GNU ассемблер.p2alignДиректива дает вам некоторое условное поведение: .p2align 4,,10 будет выравниваться до 16 (1<<4), но только если это пропускает 10 байтов или меньше. (Пустой 2-й аргумент означает, что заполнитель - NOP, а имя выравнивания степени 2 - потому, что просто .align на некоторых платформах имеет значение power-2, а на других - в байтах. gcc часто испускает это перед вершиной циклов:

  .p2align 4,,10 
  .p2align 3
.L7:

Таким образом, вы всегда получаете 8-байтовое выравнивание (безусловное .p2align 3), но, возможно, также 16, если это не будет тратить больше, чем 10B. Прежде всего, важно поместить большее выравнивание, чтобы избежать получения, например, 1-байтовой NOP, а затем 8-байтовой NOP вместо одной 9-байтовой NOP.

Вероятно, возможно реализовать эту функцию с помощью макроса NASM.


Отсутствующие функции, которых нет у ассемблера (AFAIK):

  • Директива для дополнения предыдущих инструкций с использованием более длинных кодировок (например, imm32 вместо imm8 или ненужных префиксов REX) для достижения желаемого выравнивания без NOP.
  • Умные условные вещи, основанные на длине следующих инструкций, например, не дополняющие, если 4 инструкции могут быть декодированы до достижения следующей границы 16B или 32B.

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


Особенно, если у вас есть возможность настраивать ограниченный набор процессоров, тестируйте и не падайте, если вы не найдете идеальной выгоды. Во многих случаях, особенно для процессоров с кэшем UOP и / или буфером цикла, можно не выравнивать цели ветвления внутри функций, даже циклов.


Некоторые из изменений производительности из-за изменяющегося выравнивания состоят в том, что это делает различные ветви псевдонимами друг друга в кэшах предсказания ветвления. Этот вторичный тонкий эффект все еще присутствует, даже когда кэш uop работает отлично, и нет никаких узких мест переднего плана при извлечении в основном пустых строк из кэша uop.

См. Также Оптимизация производительности сборки x86-64 - выравнивание и прогноз ветвления

Другие вопросы по тегам