Снижается ли производительность при выполнении циклов, чей счетчик uop не кратен ширине процессора?

Мне интересно, как циклы различных размеров выполняются на последних процессорах x86, как функция количества uops.

Здесь цитата из Питера Кордеса, которая подняла вопрос о нечетных числах в другом вопросе:

Я также обнаружил, что ширина канала uop из буфера цикла не является постоянная 4 за цикл, если цикл не кратен 4 мкп. (Т.е. это abc, abc,...; а не abca, bcab,...). Agner Fog microarch doc к сожалению, не было ясно, что это ограничение буфера цикла.

Проблема заключается в том, должны ли циклы быть кратными N uops для выполнения с максимальной пропускной способностью uop, где N - ширина процессора. (т.е. 4 для последних процессоров Intel). Есть много осложняющих факторов, когда речь идет о "ширине" и подсчете голосов, но я в основном хочу их игнорировать. В частности, не предполагайте микро- или макро-слияния.

Питер дает следующий пример цикла с 7 uops в своем теле:

В 7-футовом цикле будут выпущены группы из 4 | 3 | 4 | 3 |... Я не тестировал больше петли (которые не вписываются в буфер цикла), чтобы увидеть, возможно ли это для первая инструкция из следующей итерации для выпуска в том же как принятая ветвь к ней, но я предполагаю, что нет.

В более общем случае утверждение состоит в том, что каждая итерация цикла с x uops в его теле займет не менее ceil(x / 4) итераций, а не просто x / 4.

Это верно для некоторых или всех последних x86-совместимых процессоров?

Ответы

Ответ 1

Я провел некоторое исследование с Linux perf чтобы помочь ответить на этот вопрос на моем Skylake i7-6700HQ, и результаты Haswell были любезно предоставлены другим пользователем. Приведенный ниже анализ относится к Skylake, но за ним следует сравнение с Haswell.

Другие архитектуры могут отличаться от 0 и чтобы помочь разобраться во всем, я приветствую дополнительные результаты. Источник доступен).

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

Краткое изложение правил для циклического исполнения

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

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

  • Если счетчик кратен 4, то все в порядке: эти циклы выполняются оптимально.
  • Если число четное и меньше 32, то все в порядке, за исключением случаев, когда оно равно 10, и в этом случае вам следует развернуть другое четное число, если можете.
  • Для нечетных чисел вы должны попытаться развернуть на четное число меньше 32 или кратное 4, если можете.
  • Если число циклов больше 32 моп, но меньше 64, вы можете развернуть его, если оно не кратно 4: с более чем 64 мопами вы получите эффективную производительность при любом значении на Sklyake и почти на всех значениях на Haswell ( с некоторыми отклонениями, возможно, связанные с выравниванием). Неэффективность для этих циклов все еще относительно мала: значения, которых следует избегать, - это счет 4N + 1, а затем счет 4N + 2.

Резюме выводов

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

Для кода, обработанного унаследованными декодерами, верно обратное: время выполнения цикла ограничено целым числом циклов, и, следовательно, циклы, не кратные 4 мопам, не могут достичь 4 моп/цикл, поскольку они тратят некоторые слоты выдачи/выполнения,

Для кода, выданного из детектора петлевого потока (LSD), ситуация представляет собой смесь двух и более подробно поясняется ниже. В целом, циклы менее 32 мопов и с четным числом мопов выполняются оптимально, в то время как циклы нечетного размера - нет, а для больших циклов требуется оптимальное число миль-4.

Что говорит Intel

У Intel есть примечание об этом в их руководстве по оптимизации, подробности в другом ответе.

подробности

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

Legacy Decoder

Устаревший декодер 1 - это полный декодер машинного кода в uops, который используется 2, когда код не помещается в механизмы кэширования uop (LSD или DSB). Основная причина этого может состоять в том, что рабочий набор кода больше, чем кэш-память UOP (в идеальном случае приблизительно ~ 1500 моп, а на практике меньше). Однако для этого теста мы воспользуемся преимуществом того факта, что устаревший декодер также будет использоваться, если выровненный 32-байтовый блок содержит более 18 инструкций 3.

Чтобы проверить поведение устаревшего декодера, мы используем цикл, который выглядит следующим образом:

short_nop:
    mov rax, 100_000_000
ALIGN 32
.top:
    dec rax
    nop
    ...
    jnz .top
    ret

По сути, тривиальный цикл, который ведет rax отсчет, пока rax станет равным нулю. Все инструкции представляют собой один шаг 4, а количество команд nop варьируется (в месте, указанном как ...) для проверки циклов разных размеров (поэтому цикл с 4 циклами будет иметь 2 nop плюс инструкции управления двумя циклами). Здесь нет макро-слияния, так как мы всегда разделяем dec и jnz хотя бы с одним nop, а также без микро-слияния. Наконец, нет доступа к памяти в (вне подразумеваемого доступа icache).

Обратите внимание, что этот цикл очень плотный - около 1 байта на инструкцию (поскольку инструкции nop по 1 байту каждая), поэтому мы запустим> 18 инструкций в состоянии блока 32B, как только нажмем 19 инструкций в цикле. Основываясь на проверке счетчиков производительности perf lsd.uops и idq.mite_uops, мы получаем именно то, что видим: по существу, 100% инструкций выводятся из LSD 5 вплоть до цикла 18 моп, включая 100 моп и более, 100% родом из устаревшего декодера.

В любом случае, вот циклы/итерации для всех размеров цикла от 3 до 99 моп 6:

Cyles/iteration for loops with given size

Синие точки - это циклы, которые вписываются в ЛСД и демонстрируют довольно сложное поведение. Мы рассмотрим это позже.

Красные точки (начиная с 19 моп/итерация) обрабатываются устаревшим декодером и показывают очень предсказуемый шаблон:

  • Все циклы с N uops занимают точно ceiling(N/4) итерации

Так, по крайней мере, для унаследованного декодера, наблюдение Питера выполняется точно на Skylake: циклы с кратным 4 мопам могут выполняться при IPC 4, но любое другое количество мопов будет тратить 1, 2 или 3 слота выполнения (для лупов с 4N+3, 4N+2, 4N+1 инструкции соответственно).

Мне не понятно, почему это происходит. Хотя это может показаться очевидным, если учесть, что декодирование происходит в смежных блоках 16B, и поэтому при скорости декодирования 4 цикла в jnz/цикле, не кратной 4, всегда будут иметь некоторые конечные (потерянные) слоты в цикле, с которым jnz инструкция jnz, Однако фактический модуль выборки и декодирования состоит из фаз предварительного кодирования и декодирования с промежуточной очередью. Фаза предварительного кода фактически имеет пропускную способность 6 команд, но декодирует только до конца 16-байтовой границы в каждом цикле. Похоже, это подразумевает, что пузырь, возникающий в конце цикла, может быть поглощен очередью предварительного кодирования → декодирования, так как предварительный кодер имеет среднюю пропускную способность выше 4.

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

Вышеприведенный тест показывает поведение, при котором верхняя часть цикла выравнивается по 32-байтовой границе. Ниже приведен тот же график, но с добавленной серией, которая показывает эффект, когда вершина цикла перемещается на 2 байта вверх (т.е. теперь смещена по границе 32N + 30):

Legacy decoder cycles/iteration when misaligned

Большинство циклов теперь имеют штраф в 1 или 2 цикла. Случай с 1 штрафом имеет смысл, когда вы рассматриваете декодирование границ 16B и декодирование с 4 командами за цикл, и случаи с 2 штрафами за цикл происходят для циклов, где по какой-то причине DSB используется для 1 команды в цикле (вероятно, появляется команда dec которая появляется в отдельном 32-байтовом блоке) и некоторых штрафах DSB <-> MITE.

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

DSB (Uop Cache)

Кэш UOP (Intel любит называть его DSB) способен кэшировать большинство циклов умеренного количества инструкций. В типичной программе вы надеетесь, что большинство ваших инструкций будут отправлены из этого кэша 7.

Мы можем повторить тест выше, но теперь обслуживаем мопы из кеша мопов. Это простой вопрос увеличения размера наших nops до 2 байтов, поэтому мы больше не достигаем ограничения в 18 инструкций. Мы используем 2-байтовый nop xchg ax, ax в нашем цикле:

long_nop_test:
    mov rax, iters
ALIGN 32
.top:
    dec eax
    xchg ax, ax  ; this is a 2-byte nop
    ...
    xchg ax, ax
    jnz .top
    ret

Здесь результаты очень просты. Для всех протестированных размеров циклов, доставленных из DSB, требуемое количество циклов составляло N/4 т.е. Циклы выполнялись с максимальной теоретической пропускной способностью, даже если у них не было кратных 4 мопам. Таким образом, в общем случае на Skylake циклы умеренного размера, обслуживаемые из DSB, не должны беспокоиться о том, чтобы число мопов соответствовало определенному кратному.

Здесь график до 1000 моп циклов. Если вы щуритесь, вы можете увидеть неоптимальное поведение до 64 мопов (когда цикл находится в ЛСД). После этого, это прямой выстрел, 4 IPC весь путь до 1000 мопов (с проблеском около 900, который, вероятно, был из-за нагрузки на мой ящик):

Cycle counts for loops served out of the DSB

Далее мы рассмотрим производительность для циклов, которые достаточно малы, чтобы поместиться в кэш UOP.

LSD (паровой детектор циклы)

Важное примечание: Корпорация Intel, по-видимому, отключила LSD на микросхемах Skylake (SKR150 Erratum) и Kaby Lake (KBL095, KBW095 Erratum) с помощью обновления микрокода и из коробки из- за ошибки, связанной с взаимодействием между гиперпоточностью и ЛСД. Для этих чипов на приведенном ниже графике, вероятно, не будет интересного региона до 64 моп; скорее это будет выглядеть так же, как регион после 64 мопов.

Детектор петлевого потока может кэшировать небольшие циклы до 64 мопов (на Skylake). В недавней документации Intel он позиционируется скорее как механизм энергосбережения, чем как функция производительности, хотя при использовании LSD, безусловно, не упоминается о недостатках производительности.

Запустив это для размеров цикла, которые должны вписываться в LSD, мы получим следующее поведение циклов/итерации:

Cycles per Iteration for LSD-resident loops

Красная линия - это процент мопов, доставленных с ЛСД. Он выравнивается на 100% для всех размеров петель от 5 до 56 моп.

Для циклов 3 и 4 мопов мы имеем необычное поведение: 16% и 25% мопов, соответственно, доставляются из устаревшего декодера. А? К счастью, это не влияет на пропускную способность цикла, поскольку в обоих случаях достигается максимальная пропускная способность в 1 цикл/цикл - несмотря на то, что можно ожидать некоторых штрафов за переход MITE <-> LSD.

Между размерами петель от 57 до 62 мопов количество мопов, доставленных из ЛСД, демонстрирует странное поведение - примерно 70% мопов доставляется из ЛСД, а остальные из ЦСБ. Skylake номинально имеет LSD с 64 мопами, так что это какой-то переход прямо перед превышением размера LSD - возможно, есть какое-то внутреннее выравнивание внутри IDQ (на котором реализован LSD), которое вызывает только частичные попадания в ЛСД на этом этапе. Эта фаза короткая и, с точки зрения производительности, в основном представляет собой линейную комбинацию производительности full-in-LSD, которая предшествует ей, и производительности full-in-DSB, которая следует за ней.

Давайте посмотрим на основную часть результатов от 5 до 56 моп. Мы видим три разных региона:

Циклы от 3 до 10 мопов: здесь поведение сложное. Это единственная область, в которой мы видим количество циклов, которое нельзя объяснить статическим поведением за одну итерацию цикла 8. Диапазон достаточно короткий, поэтому сложно сказать, есть ли закономерность. Циклы с 4, 6 и 8 мопами выполняются оптимально, в N/4 циклах (тот же шаблон, что и в следующем регионе).

Цикл из 10 мопов, с другой стороны, выполняется за 2,66 циклов за итерацию, что делает его единственным четным размером цикла, который не выполняется оптимально, пока вы не достигнете размеров цикла от 34 моп или выше (кроме выброса в 26), Это соответствует чему-то похожему на частоту повторения мопов/циклов 4, 4, 4, 3. Для цикла из 5 мопов вы получаете 1,33 цикла на итерацию, очень близко, но не то же самое, что идеал 1,25. Это соответствует скорости выполнения 4, 4, 4, 4, 3.

Эти результаты трудно объяснить. Результаты повторяются от запуска к запуску и устойчивы к изменениям, таким как замена nop для инструкции, которая фактически выполняет что-то вроде mov ecx, 123. Это может быть связано с лимитом в 1 взятую ветвь каждые 2 цикла, который применяется ко всем циклам, кроме тех, которые "очень малы". Может случиться так, что мопы иногда выстраиваются так, что это ограничение срабатывает, приводя к дополнительному циклу. Как только вы достигнете 12 моп или выше, это никогда не произойдет, так как вы всегда берете как минимум три цикла на итерацию.

Петли от 11 до 32 мопов: мы видим ступенчатую схему, но с периодом два. В основном, все циклы с четным числом мопов работают оптимально - т.е. принимают ровно N/4 цикла. Циклы с нечетным числом мопов тратят один "выпускной слот" и принимают то же количество циклов, что и цикл с еще одним мопом (т.е. Цикл из 17 моп занимает те же 4,5 цикла, что и цикл из 18 моп). Таким образом, здесь мы имеем поведение лучше, чем ceiling(N/4) для многих подсчетов мопов, и у нас есть первое доказательство того, что Skylake по крайней мере может выполнять циклы за нецелое число циклов.

Единственными выбросами являются N = 25 и N = 26, которые занимают примерно на 1,5% больше времени, чем ожидалось. Это небольшой, но воспроизводимый и устойчивый к перемещению функции в файле. Это слишком мало, чтобы быть объясненным эффектом пертерации, если только у него нет гигантского периода, так что, вероятно, что-то еще.

Общее поведение здесь точно соответствует (за исключением аномалии 25/26) с аппаратным обеспечением, развертывающим цикл в 2 раза.

Циклы от 33 до ~ 64 моп. Мы снова видим схему ступенек, но с периодом 4 и худшей средней производительностью, чем в случае до 32 моп. Поведение точно ceiling(N/4) то есть то же, что и унаследованный корпус декодера. Таким образом, для циклов от 32 до 64 моп, LSD не дает очевидного преимущества по сравнению с традиционными декодерами с точки зрения пропускной способности внешнего интерфейса для этого конкретного ограничения. Конечно, есть много других способов улучшить LSD - он избегает многих потенциальных узких мест декодирования, которые возникают для более сложных или более длинных команд, и экономит электроэнергию и т.д.

Все это довольно удивительно, потому что это означает, что циклы, доставляемые из кэша UOP, обычно работают лучше во внешнем интерфейсе, чем циклы, доставляемые из LSD, несмотря на то, что LSD обычно позиционируется как строго лучший источник UOP, чем DSB (например, как часть совета, старайтесь держать циклы достаточно маленькими, чтобы поместиться в ЛСД).

Вот еще один способ взглянуть на те же данные - с точки зрения потери эффективности для данного количества мопов по сравнению с теоретической максимальной пропускной способностью 4 моп за цикл. 10% эффективности означает, что у вас есть только 90% пропускной способности, которую вы рассчитываете по простой формуле N/4.

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

Efficiency Loss by Loop Size

Три области, обсужденные выше, окрашены по-разному, и, по крайней мере, видны конкурирующие эффекты:

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

  2. При переходе в область мопов 33+ наблюдается значительный скачок неэффективности: увеличивается как размер потери пропускной способности, так и число затронутых мопов увеличивается вдвое.

  3. Первый регион несколько хаотичен, и 7 моп - это наихудшее общее количество мопов.

центровка

Приведенный выше анализ DSB и LSD относится к элементам цикла, выровненным по 32-байтовой границе, но случай с невыровненными значениями, похоже, не страдает ни в одном случае: нет существенных отличий от случая с выравниванием (кроме, возможно, небольшого отклонения) менее 10 мопов, которые я больше не расследовал).

Вот результаты без 32N-2 для 32N-2 и 32N+2 (т. 32N+2 Верхние 2 байта цикла до и после границы 32B):

Misaligned Cycles per Iteration

Идеальная линия N/4 также показана для справки.

Haswell

Далее следуем взглянуть на предыдущую микроархитектуру: Haswell. Цифры здесь любезно предоставлены пользователем Iwillnotexist Idonotexist.

LSD + Legacy Decode Pipeline

Во-первых, результаты теста "плотного кода", который проверяет LSD (для малых количеств uop) и устаревший конвейер (для больших количеств uop, поскольку цикл "вылетает" из DSB из-за плотности команд.

Сразу же мы видим разницу с точки зрения того, когда каждая архитектура доставляет мопы из LSD для плотного цикла. Ниже мы сравниваем Skylake и Haswell для коротких циклов плотного кода (1 байт на инструкцию).

Haswell vs Skylake LSD Delivery %

Как описано выше, цикл Skylake перестает доставляться из LSD ровно с 19 моп, как и ожидалось из области ограничения кода 18-моп на 32 байта. С другой стороны, Haswell, похоже, прекратил надежную доставку из ЛСД для петель 16 и 17 мопов. У меня нет никаких объяснений этому. Существует также различие в случае с 3 мегапикселями: странным образом оба процессора доставляют только некоторые свои мопы из ЛСД в случаях 3 и 4 моп, но точное количество одинаково для 4 моп и отличается от 3.

Мы в основном заботимся о реальных показателях, правда? Итак, давайте посмотрим на циклы/итерации для случая плотного кода с 32-байтовым выравниванием:

Haswell vs Skylake LSD + Legacy Pipeline

Это те же данные, что и выше, для Скайлэйка (выровненный ряд был удален), вместе с Хасвеллом. Сразу же вы замечаете, что картина для Haswell похожа, но не такая. Как и выше, здесь есть два региона:

Legacy Decode

Циклы, большие чем ~ 16-18 моп (неопределенность описана выше), доставляются от устаревших декодеров. Шаблон для Haswell несколько отличается от Skylake.

Для диапазона от 19 до 30 мопов они идентичны, но после этого Хасвелл нарушает схему. Skylake взял циклы ceil(N/4) для циклов, доставленных от устаревших декодеров. Haswell, с другой стороны, похоже, принимает что-то вроде ceil((N+1)/4) + ceil((N+2)/12) - ceil((N+1)/12). Хорошо, этот беспорядок (короче, кто-нибудь?) - но в основном это означает, что в то время как Skylake выполняет циклы с 4 * N циклами оптимально (т.е. При 4 моп/цикл), такие циклы (локально) обычно являются наименьшим оптимальным числом (по крайней мере, локально) - для выполнения таких циклов требуется больше цикла, чем в Skylake. Таким образом, на самом деле вам лучше всего использовать циклы 4N-1 мопов в Haswell, за исключением того, что 25% таких петель, которые также имеют форму 16-1N (31, 47, 63 и т.д.), Выполняют один дополнительный цикл. Это начинает звучать как расчет високосного года - но модель, вероятно, лучше всего понять визуально выше.

Я не думаю, что эта модель присуща распространению Uop на Haswell, поэтому мы не должны вдаваться в подробности. Кажется, это объясняется

0000000000455a80 <short_nop_aligned35.top>:
16B cycle
  1     1 455a80:       ff c8   dec    eax
  1     1 455a82:       90      nop
  1     1 455a83:       90      nop
  1     1 455a84:       90      nop
  1     2 455a85:       90      nop
  1     2 455a86:       90      nop
  1     2 455a87:       90      nop
  1     2 455a88:       90      nop
  1     3 455a89:       90      nop
  1     3 455a8a:       90      nop
  1     3 455a8b:       90      nop
  1     3 455a8c:       90      nop
  1     4 455a8d:       90      nop
  1     4 455a8e:       90      nop
  1     4 455a8f:       90      nop
  2     5 455a90:       90      nop
  2     5 455a91:       90      nop
  2     5 455a92:       90      nop
  2     5 455a93:       90      nop
  2     6 455a94:       90      nop
  2     6 455a95:       90      nop
  2     6 455a96:       90      nop
  2     6 455a97:       90      nop
  2     7 455a98:       90      nop
  2     7 455a99:       90      nop
  2     7 455a9a:       90      nop
  2     7 455a9b:       90      nop
  2     8 455a9c:       90      nop
  2     8 455a9d:       90      nop
  2     8 455a9e:       90      nop
  2     8 455a9f:       90      nop
  3     9 455aa0:       90      nop
  3     9 455aa1:       90      nop
  3     9 455aa2:       90      nop
  3     9 455aa3:       75 db   jne    455a80 <short_nop_aligned35.top>

Здесь я отметил блок декодирования 16B (1-3), в котором появляется каждая инструкция, и цикл, в котором она будет декодирована. Правило в основном состоит в том, что до следующих 4 инструкций декодируются, пока они попадают в текущий блок 16B. В противном случае они должны ждать до следующего цикла. Для N = 35 мы видим, что в цикле 4 потеряно 1 слот декодирования (в блоке 16B осталось только 3 инструкции), но в противном случае цикл очень хорошо выровнен с границами 16B и даже с последним циклом ( 9) может декодировать 4 инструкции.

Здесь усеченный взгляд на N = 36, который идентичен за исключением конца цикла:

0000000000455b20 <short_nop_aligned36.top>:
16B cycle
  1     1 455a80:       ff c8   dec    eax
  1     1 455b20:       ff c8   dec    eax
  1     1 455b22:       90      nop
  ... [29 lines omitted] ...
  2     8 455b3f:       90      nop
  3     9 455b40:       90      nop
  3     9 455b41:       90      nop
  3     9 455b42:       90      nop
  3     9 455b43:       90      nop
  3    10 455b44:       75 da   jne    455b20 <short_nop_aligned36.top>

Теперь есть 3 инструкции для декодирования в 3-м и последнем фрагменте 16B, поэтому необходим один дополнительный цикл. В основном, 35 команд для этого конкретного шаблона команд лучше соответствуют границам 16B битов и сохраняют один цикл при декодировании. Это не значит, что N = 35 лучше, чем N = 36 в целом! Разные инструкции будут иметь разное количество байтов и будут выстраиваться по-разному. Подобная проблема выравнивания объясняет также дополнительный цикл, который требуется каждые 16 байтов:

16B cycle
...
  2     7 45581b:       90      nop
  2     8 45581c:       90      nop
  2     8 45581d:       90      nop
  2     8 45581e:       90      nop
  3     8 45581f:       75 df   jne    455800 <short_nop_aligned31.top>

Здесь последний jne проскользнул в следующий блок 16B (если инструкция выходит за границу 16B, он фактически находится в последнем фрагменте), вызывая дополнительную потерю цикла. Это происходит только каждые 16 байтов.

Таким образом, результаты унаследованного декодера Haswell прекрасно объясняются унаследованным декодером, который ведет себя так, как описано, например, в документе по микроархитектуре Agner Fog. Фактически, это также объясняет результаты Skylake, если предположить, что Skylake может декодировать 5 команд за цикл (обеспечивая до 5 моп) 9. Предполагая, что это возможно, пропускная способность асимптотического устаревшего декодирования в этом коде для Skylake по-прежнему равна 4 мопам, так как блок из 16 нопов декодирует 5-5-5-1, а не 4-4-4-4 в Haswell, поэтому вы получаете только Преимущества по краям: в случае N = 36, например, Skylake может декодировать все оставшиеся 5 команд против 4-1 для Haswell, сохраняя цикл.

В результате кажется, что обычное поведение декодера можно понять довольно простым способом, и главный совет по оптимизации заключается в том, чтобы продолжать наращивать код так, чтобы он "умело" попадал в фрагменты, выровненные по 16B (возможно, этот NP- тяжело как упаковка бина?).

DSB (и снова LSD)

Теперь давайте взглянем на сценарий, в котором код подается из LSD или DSB - с помощью теста "long nop", который избегает превышения лимита порции 18-моп на 32B и таким образом остается в DSB.

Haswell против Skylake:

Haswell vs Skylake LSD and DSB

Обратите внимание на поведение LSD - здесь Haswell прекращает подачу из LSD ровно с 57 моп, что полностью соответствует опубликованному размеру LSD в 57 моп. Там нет странного "переходного периода", как мы видим на Skylake. Haswell также имеет странное поведение для 3 и 4 мопов, где только ~ 0% и ~ 40% мопов, соответственно, происходят от ЛСД.

Что касается производительности, Haswell обычно соответствует Skylake с несколькими отклонениями, например, около 65, 77 и 97 моп, где он округляется до следующего цикла, тогда как Skylake всегда способен выдержать 4 моп/цикл, даже когда это приводит в нецелом числе циклов. Небольшое отклонение от ожидаемого в 25 и 26 моп исчезло. Возможно, 6-мегапиксельная скорость доставки Skylake поможет ему избежать проблем с выравниванием моп-кэша, которые Haswell страдает со своей 4-мегапиксельной скоростью доставки.

Другие Архитектуры

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

Нужна помощь

Хотя результаты для многих платформ были любезно предоставлены сообществом, мне все еще интересны результаты на чипах старше Nehalem и новее, чем Coffee Lake (в частности, Cannon Lake, который является новым uarch). Код для генерации этих результатов является общедоступным. Кроме того, вышеприведенные результаты доступны также в формате .ods в GitHub.


0 В частности, в наследство максимальный Декодер пропускная по- видимому, увеличилась с 4 до 5 микрооперации в Skylake, а максимальная пропускная способность для кэша микроопераций увеличен с 4 до 6. Оба эти могут повлиять на результаты, описанные здесь.

1 Корпорация Intel на самом деле любит называть устаревший декодер MITE (механизм перевода микро-инструкций), возможно, потому, что это сложная задача - на самом деле пометить любую часть вашей архитектуры устаревшей коннотацией.

2 Технически существует другой, даже более медленный, источник мопов - MS (механизм секвенирования микрокодов), который используется для реализации любой команды с более чем 4 мопами, но мы здесь это игнорируем, поскольку ни один из наших циклов не содержит микрокодированных инструкций.

3 Это работает, потому что любой выровненный 32-байтовый блок может использовать не более 3-х каналов в своем слоте UOP-кэша, и каждый слот может содержать до 6 UOP. Так что если вы используете более 3 * 6 = 18 мопов в 32-битном блоке, код вообще не может быть сохранен в кеш мопов. Вероятно, на практике это редко встречается, поскольку код должен быть очень плотным (менее 2 байт на инструкцию), чтобы вызвать это.

4 Инструкции nop декодируются до одного мопа, но не исключаются до выполнения (т.е. Они не используют порт выполнения), но все еще занимают место во внешнем интерфейсе и, таким образом, учитывают различные ограничения, которыми мы являемся увлекающийся.

5 LSD - это детектор петлевого потока, который кэширует небольшие циклы до 64 мопов (Skylake) непосредственно в IDQ. В более ранних архитектурах он может содержать 28 мопов (оба логических ядра активны) или 56 мопов (одно активное логическое ядро).

6 Мы не можем легко вписать цикл 2 uop в этот шаблон, так как это будет означать, что инструкции nop означают, что инструкции dec и jnz будут jnz макросе с соответствующим изменением счетчика uop. Просто поверьте мне на слово, что все циклы с 4 или менее мопами выполняются в лучшем случае за 1 цикл/итерацию.

7 Для интереса я просто запустил perf stat с небольшим прогоном Firefox, где открыл вкладку и щелкнул несколько вопросов о переполнении стека. За доставленные инструкции я получил 46% от DSB, 50% от устаревшего декодера и 4% для LSD. Это показывает, что по крайней мере для большого, ветвящегося кода, такого как браузер, DSB все еще не может захватить большую часть кода (к счастью, унаследованные декодеры не так уж плохи).

8 Под этим я подразумеваю, что все остальные циклы можно объяснить, просто взяв "эффективную" стоимость интегрального цикла в мопах (которая может быть больше, чем фактический размер мопов) и разделив на 4. Для этих очень коротких петель это не работает - вы не можете добраться до 1,333 циклов за итерацию, разделив любое целое число на 4. Иначе говоря, во всех других регионах затраты имеют форму N/4 для некоторого целого числа N.

9 На самом деле мы знаем, что Skylake может доставлять 5 моп за цикл от устаревшего декодера, но мы не знаем, могут ли эти 5 моп поступать из 5 разных инструкций или только из 4 или менее. То есть мы ожидаем, что Skylake сможет декодировать по схеме 2-1-1-1, но я не уверен, сможет ли он декодировать по схеме 1-1-1-1-1. Приведенные выше результаты свидетельствуют о том, что он действительно может декодировать 1-1-1-1-1.

Ответ 2

Это продолжение первоначального ответа, чтобы проанализировать поведение для пяти дополнительных архитектур на основе результатов теста, представленных Андреасом Абелем:

  • Nehalem
  • Песчаный Мост
  • Ivy Bridge
  • Бродуэлла
  • Кофейное озеро

Мы кратко рассмотрим результаты на этих архитектурах в дополнение к Skylake и Haswell. Это только должен быть "быстрый" взгляд, так как все архитектуры, кроме Nehalem, следуют одному из существующих шаблонов, обсужденных выше.

Во-первых, короткий случай nop, в котором используется устаревший декодер (для циклов, которые не вписываются в LSD) и LSD. Вот циклы/итерации для этого сценария для всех 7 архитектур.

Рисунок 2.1: Плотная производительность всех архитектур:

All Architectures Dense Nop Performance

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

Во-первых, давайте обсудим большой выброс: Нехалем. Все остальные архитектуры имеют наклон, который примерно следует линии 4 мопа/цикла, но Nehalem имеет почти ровно 3 мопа за цикл, поэтому быстро отстает от всех других архитектур. За пределами начальной области ЛСД линия также является абсолютно гладкой, без появления "ступеньки" в других архитектурах.

Это полностью согласуется с тем, что Nehalem имеет предел выхода на пенсию в меру 3 моп/цикл. Это узкое место для мопов за пределами ЛСД: все они выполняют примерно по 3 мопа за цикл, узкое место при выходе на пенсию. Внешний интерфейс не является узким местом, поэтому точный счетчик мопов и порядок декодирования не имеют значения, поэтому ступенька отсутствует.

Кроме Nehalem, другие архитектуры, кроме Broadwell, довольно четко разделены на группы: Haswell-подобные или Skylake-подобные. То есть все Sandy Bridge, Ivy Bridge и Haswell ведут себя как Haswell, для циклов больше, чем около 15 мопов (поведение Haswell обсуждается в другом ответе). Несмотря на то, что они представляют собой разные микроархитектуры, они ведут себя в основном одинаково, поскольку их традиционные возможности декодирования одинаковы. Ниже примерно 15 мопов мы видим, что Haswell несколько быстрее для любого количества мертвеев, не кратного 4. Возможно, он получает дополнительное развёртывание в LSD из-за большего LSD, или есть другие оптимизации "малого цикла". Для Sandy Bridge и Ivy Bridge это означает, что маленькие циклы должны определенно нацеливаться на число мопов, кратное 4.

Кофейное озеро ведет себя подобно Skylake 1. Это имеет смысл, поскольку микроархитектура одинакова. Кофе Лейк выглядит лучше, чем Скайлэйк, ниже примерно 16 мопов, но это всего лишь эффект от того, что Кофе Лейк отключен ЛСД по умолчанию. Skylake был протестирован с включенным LSD, прежде чем Intel отключил его с помощью обновления микрокода из-за проблемы безопасности. Кофейное Озеро было выпущено после того, как эта проблема была известна, так что ЛСД был отключен из коробки. Таким образом, для этого теста Coffee Lake использует либо DSB (для циклов ниже примерно 18 моп, который все еще может поместиться в DSB), либо устаревший декодер (для остальных циклов), что приводит к лучшим результатам при небольшом числе мопов. Циклы, где LSD накладывает накладные расходы (интересно, что для больших циклов LSD и устаревший декодер накладывают одинаковые издержки по очень разным причинам).

Наконец, мы рассмотрим 2-байтовые NOP, которые недостаточно плотны, чтобы предотвратить использование DSB (поэтому этот случай больше отражает типичный код).

Рисунок 2.1: производительность 2-байтового nop:

2-byte nop performance

Опять же, результат в том же направлении, что и на предыдущем графике. Nehalem по-прежнему является узким местом с 3 мопами за цикл. Для диапазона до примерно 60-ти мопов все архитектуры, кроме Coffee Lake, используют LSD, и мы видим, что Sandy Bridge и Ivy Bridge работают немного хуже, округляясь до следующего цикла и, таким образом, достигая максимальной пропускной способности 4 моп/цикл, если число мопов в цикле кратно 4. Выше 32 мопов функция "развёртывания" в Haswell и новых уархах не имеет никакого эффекта, поэтому все примерно привязано.

Sandy Bridge на самом деле имеет несколько диапазонов мопов (например, от 36 до 44 мопов), где он работает лучше, чем новые архитектуры. Похоже, это происходит потому, что не все циклы обнаруживаются LSD, и в этих диапазонах циклы обслуживаются вместо DSB. Поскольку DSB, как правило, быстрее, то и Sandy Bridge в этих случаях.

Что говорит Intel

На самом деле вы можете найти раздел, специально посвященный этой теме, в Руководстве по оптимизации Intel, раздел 3.4.2.5, как отметил Андреас Абель в комментариях. Там Intel говорит:

LSD содержит микрооперации, которые создают маленькие "бесконечные" циклы. Микрооперации от ЛСД распределяются в неработающем двигателе. Цикл в LSD заканчивается взятой ветвью к началу цикла. Взятая ветвь в конце цикла всегда является последней микрооперацией, выделенной в цикле. Инструкция в начале цикла всегда выделяется в следующем цикле. Если производительность кода ограничена полосой пропускания внешнего интерфейса, неиспользуемые временные интервалы выделения приводят к появлению пузыря в распределении и могут привести к снижению производительности degrada-. Пропускная способность распределения в кодовом названии микроархитектуры Intel Sandy Bridge составляет четыре микрооперации за цикл. Производительность лучше всего, когда количество микроопераций в ЛСД приводит к наименьшему количеству неиспользованных allo- катионных слотов. Вы можете использовать развёртывание цикла, чтобы контролировать количество микроопераций в LSD.

Далее они показывают пример, в котором развертывание цикла в два раза не способствует повышению производительности из-за "округления" LSD, но разворачивается тремя работами. Этот пример сильно сбивает с толку, поскольку он фактически смешивает два эффекта, так как развертывание more также уменьшает накладные расходы цикла и, следовательно, количество мопов за итерацию. Более интересный пример был бы, когда развертывание цикла меньше раз приводило к увеличению производительности из-за эффектов округления LSD.

Этот раздел, кажется, точно описывает поведение в Sandy Bridge и Ivy Bridge. Приведенные выше результаты показывают, что обе эти архитектуры работают так, как описано, и вы теряете 1, 2 или 3 слота выполнения мопов для циклов с 4N + 3, 4N + 2 или 4N + 1 мопов соответственно.

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


1 В 16 мопах есть странный выброс, когда Coffee Lake работает хуже, чем все другие архитектуры, даже Nehalem (регрессия около 50%), но, может быть, это шум измерения?