Понимание влияния lfence на петлю с двумя длинными цепями зависимостей, для увеличения длины - программирование
Подтвердить что ты не робот

Понимание влияния lfence на петлю с двумя длинными цепями зависимостей, для увеличения длины

Я играл с кодом в этом ответе, слегка меняя его:

BITS 64

GLOBAL _start

SECTION .text

_start:
 mov ecx, 1000000

.loop:

 ;T is a symbol defined with the CLI (-DT=...)

 TIMES T imul eax, eax
 lfence
 TIMES T imul edx, edx


 dec ecx
jnz .loop

 mov eax, 60           ;sys_exit
 xor edi, edi
 syscall

Без lfence я результаты, которые я получаю, согласуются с статическим анализом в этом ответе.

Когда я lfence единый lfence я ожидаю, что CPU выполнит последовательность imul edx, edx k-й итерации параллельно с imul eax, eax последовательностью следующей (k + 1-й) итерации.
Что-то вроде этого (вызов A imul eax, eax sequence и D imul edx, edx one):

|
| A
| D A
| D A
| D A
| ...
| D A
| D
|
V time

Принимая более или менее одинаковое количество циклов, но для одного непарного параллельного выполнения.

Когда я измеряю количество циклов, для исходной и модифицированной версии, с taskset -c 2 ocperf.py stat -r 5 -e cycles:u '-x './main-$T для T в диапазон ниже я получаю

T   Cycles:u    Cycles:u    Delta
    lfence      no lfence

10  42047564    30039060    12008504
15  58561018    45058832    13502186
20  75096403    60078056    15018347
25  91397069    75116661    16280408
30  108032041   90103844    17928197
35  124663013   105155678   19507335
40  140145764   120146110   19999654
45  156721111   135158434   21562677
50  172001996   150181473   21820523
55  191229173   165196260   26032913
60  221881438   180170249   41711189
65  250983063   195306576   55676487
70  281102683   210255704   70846979
75  312319626   225314892   87004734
80  339836648   240320162   99516486
85  372344426   255358484   116985942
90  401630332   270320076   131310256
95  431465386   285955731   145509655
100 460786274   305050719   155735555

Plotted data of above

Как можно объяснить значения Cycles:u lfence?
Я бы ожидал, что они будут похожи на те, что у Cycles:u no lfence так как один lfence должен предотвращать параллельную реализацию первой итерации для двух блоков.
Я не думаю, что это из - за lfence накладных расходов, поскольку я считаю, что должно быть постоянным для всех T s.

Я хотел бы исправить то, что неправильно с моим forma mentis, когда речь идет о статическом анализе кода.


Поддержка репозитория с исходными файлами.

4b9b3361

Ответ 1

Я lfence анализ для случая, когда T = 1 для обоих кодов (с и без lfence). Затем вы можете расширить его для других значений T. Вы можете обратиться к рис. 2.4 Руководства по оптимизации Intel для визуального.

Поскольку существует только одна легко предсказанная ветвь, интерфейс будет только останавливаться, если бэкэнд застопорился. Интерфейс имеет 4-х уровневое значение в Haswell, что означает, что из IDQ может быть выведено до 4 плавных uops (очередь декодирования команд, которая является просто очередью, в которой хранятся uops в режиме fused-domain, также называемые очередью uop) (RS) запрашивает планировщик. Каждый imul декодируется в один uop, который нельзя слить. jnz.loop dec ecx и jnz.loop получают macrofused в интерфейсе до одного uop. Одна из отличий между микрофьюзией и макрофьюзией заключается в том, что когда планировщик отправляет макроопределенный uop (который не является микропотоком) к исполняемому модулю, которому он назначен, он отправляется как один uop. Напротив, микрофонный uop необходимо разбить на составляющие uops, каждый из которых должен быть отдельно отправлен в исполнительный блок. (Тем не менее, расщепление микрофонных uops происходит при входе в RS, а не при отправке, см. Сноску 2 в ответе @Peter). lfence декодируется в 6 часов. Признание микрофлюзии имеет значение только в бэкэнд, и в этом случае в петле нет микрофузии.

Так как ветвь цикла легко предсказуема, и поскольку число итераций относительно велико, мы можем просто предположить, не поставив под угрозу точность, что распределитель всегда сможет выделять 4 мкп за цикл. Другими словами, планировщик получит 4 часа за цикл. Поскольку нет микрофурфузии, каждый uop будет отправлен как один uop.

imul может выполняться только исполнительным блоком Slow Int (см. рис. 2.4). Это означает, что единственным выбором для выполнения imul является отправка их на порт 1. В Haswell, Slow Int прекрасно конвейерно, так что один imul может быть отправлен за цикл. Но для того, чтобы результат умножения был доступен для любой требуемой инструкции, требуется три цикла (этап обратной записи - третий цикл со стадии отправки конвейера). Таким образом, для каждой цепи зависимости не более одного imul может быть отправлено на 3 цикла.

Поскольку предсказано dec/jnz, единственным исполняющим устройством, которое может его выполнить, является Первичная ветвь на порте 6.

Поэтому в любом заданном цикле, пока RS имеет место, он получит 4 выхода. Но что это? Давайте рассмотрим цикл без привязки:

imul eax, eax
imul edx, edx
dec ecx/jnz .loop (macrofused)

Есть две возможности:

  • Два imul из одной и той же итерации, один imul из соседней итерации и один dec/jnz из одной из этих двух итераций.
  • Один dec/jnz из одной итерации, два imul из следующей итерации и один dec/jnz с той же итерации.

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

  • Отправляйте самый старый dec/jnz в порт 6 и отправляйте самый старый imul который готов к порту 1. Это всего 2 раза.
  • Поскольку Slow Int имеет задержку в 3 цикла, но есть только две цепи, для каждого цикла из 3 циклов никакие imul в RS не будут готовы к выполнению. Однако в RS всегда есть по крайней мере один dec/jnz. Поэтому планировщик может отправить это. Это всего 1 мкг.

Теперь мы можем вычислить ожидаемое количество uops в RS, X N, в конце любого заданного цикла N:

X N= X N-1 + (количество uops, которое должно быть выделено в RS в начале цикла N) - (ожидаемое количество uops, которое будет отправлено в начале цикла N)
= X N-1 + 4 - ((0 + 1) * 1/3 + (1 + 1) * 2/3)
= X N-1 + 12/3 - 5/3
= X N-1 + 7/3 для всех N> 0

Начальным условием повторения является X 0= 4. Это простой повтор, который можно решить, разворачивая X N-1.

X N= 4 + 2,3 * N для всех N> = 0

RS в Haswell имеет 60 записей. Мы можем определить первый цикл, в котором RS, как ожидается, станет полным:

60 = 4 + 7/3 * N
N = 56/2,3 = 24,3

Поэтому в конце цикла 24.3 RS, как ожидается, будет заполнен. Это означает, что в начале цикла 25.3 RS не сможет получать никаких новых uops. Теперь количество рассмотренных итераций, я рассматриваю, определяет, как вы должны продолжить анализ. Поскольку для цепочки зависимостей требуется выполнение не менее 3 * я циклов, для достижения цикла 24.3 требуется около 8,1 итераций. Поэтому, если число итераций больше 8,1, что здесь, вам нужно проанализировать, что происходит после цикла 24.3.

Планировщик отправляет инструкции со следующими тарифами каждого цикла (как обсуждалось выше):

1
2
2
1
2
2
1
2
.
.

Но распределитель не будет выделять никакие удары в RS, если имеется не менее 4 доступных записей. В противном случае он не будет тратить электроэнергию на выпуск uops на субоптимальной пропускной способности. Тем не менее, только в начале каждого 4-го цикла есть не менее 4 бесплатных записей в RS. Таким образом, начиная с цикла 24.3, распределитель, как ожидается, застопорится 3 из каждых 4 циклов.

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

Мы можем рассчитать IPC (инструкции за такт) довольно легко сейчас. Записи ROB выглядят примерно так:

imul eax, eax     -  N
imul edx, edx     -  N + 1
dec ecx/jnz .loop -  M
imul eax, eax     -  N + 3
imul edx, edx     -  N + 4
dec ecx/jnz .loop -  M + 1

Столбец справа показывает циклы, в которых инструкция может быть удалена. Выход на пенсию происходит по порядку и ограничен латентностью критического пути. Здесь каждая цепочка зависимостей имеет одинаковую длину пути и, следовательно, оба являются двумя равными критическими путями длиной 3 цикла. Таким образом, каждые 3 цикла, 4 инструкции могут быть удалены. Таким образом, IPC составляет 4/3 = 1,3, а индекс CPI равен 3/4 = 0,75. Это намного меньше теоретического оптимального МПК 4 (даже без учета micro- и макро-слияния). Поскольку выход на пенсию происходит в порядке, поведение выхода на пенсию будет одинаковым.

Мы можем проверить наш анализ, используя как perf и IACA. Я расскажу о perf. У меня есть процессор Haswell.

perf stat -r 10 -e cycles:u,instructions:u,cpu/event=0xA2,umask=0x10,name=RESOURCE_STALLS.ROB/u,cpu/event=0x0E,umask=0x1,cmask=1,inv=1,name=UOPS_ISSUED.ANY/u,cpu/event=0xA2,umask=0x4,name=RESOURCE_STALLS.RS/u ./main-1-nolfence

 Performance counter stats for './main-1-nolfence' (10 runs):

         30,01,556      cycles:u                                                      ( +-  0.00% )
         40,00,005      instructions:u            #    1.33  insns per cycle          ( +-  0.00% )
                 0      RESOURCE_STALLS.ROB                                         
         23,42,246      UOPS_ISSUED.ANY                                               ( +-  0.26% )
         22,49,892      RESOURCE_STALLS.RS                                            ( +-  0.00% )

       0.001061681 seconds time elapsed                                          ( +-  0.48% )

Всего 1 миллион итераций занимает около 3 циклов. Каждая итерация содержит 4 инструкции, а IPC - 1.33. RESOURCE_STALLS.ROB показывает количество циклов, в которых распределитель был остановлен из-за полного ROB. Это, конечно, никогда не бывает. UOPS_ISSUED.ANY можно использовать для подсчета количества выходов, выданных на РС, и количества циклов, в которых распределитель был остановлен (нет конкретной причины). Первый - простой (не показан в perf); 1 миллион * 3 = 3 миллиона + небольшой шум. Последнее гораздо интереснее. Это показывает, что около 73% всех случаев, когда распределитель застопорился из-за полного РС, что соответствует нашему анализу. RESOURCE_STALLS.RS подсчитывает количество циклов, в которых распределитель был остановлен из-за полного RS. Это близко к UOPS_ISSUED.ANY потому что распределитель не останавливается по какой-либо другой причине (хотя по какой-то причине разница может быть пропорциональна количеству итераций, мне нужно будет увидеть результаты для T> 1).

Анализ кода без lfence можно расширить, чтобы определить, что произойдет, если между двумя imul s было добавлено lfence. Пусть проверить perf результаты первого (МАА, к сожалению, не поддерживает lfence):

perf stat -r 10 -e cycles:u,instructions:u,cpu/event=0xA2,umask=0x10,name=RESOURCE_STALLS.ROB/u,cpu/event=0x0E,umask=0x1,cmask=1,inv=1,name=UOPS_ISSUED.ANY/u,cpu/event=0xA2,umask=0x4,name=RESOURCE_STALLS.RS/u ./main-1-lfence

 Performance counter stats for './main-1-lfence' (10 runs):

       1,32,55,451      cycles:u                                                      ( +-  0.01% )
         50,00,007      instructions:u            #    0.38  insns per cycle          ( +-  0.00% )
                 0      RESOURCE_STALLS.ROB                                         
       1,03,84,640      UOPS_ISSUED.ANY                                               ( +-  0.04% )
                 0      RESOURCE_STALLS.RS                                          

       0.004163500 seconds time elapsed                                          ( +-  0.41% )

Обратите внимание, что количество циклов увеличилось примерно на 10 миллионов, или 10 циклов на итерацию. Количество циклов не говорит нам о многом. Количество выбывших команд увеличилось на миллион, что ожидается. Мы уже знаем, что lfence не lfence выполнение команды, поэтому RESOURCE_STALLS.ROB не должен меняться. Особенно интересны UOPS_ISSUED.ANY и RESOURCE_STALLS.RS. В этом выводе UOPS_ISSUED.ANY подсчитывает циклы, а не uops. Можно также подсчитать количество cpu/event=0x0E,umask=0x1,name=UOPS_ISSUED.ANY/u (используя cpu/event=0x0E,umask=0x1,name=UOPS_ISSUED.ANY/u вместо cpu/event=0x0E,umask=0x1,cmask=1,inv=1,name=UOPS_ISSUED.ANY/u) и увеличилось на 6 uops на итерацию (без слияния). Это означает, что lfence который был помещен между двумя imul был декодирован в 6 uops. Вопрос в миллион долларов - это то, что делают эти uops и как они перемещаются в трубе.

RESOURCE_STALLS.RS равно нулю. Что это значит? Это указывает на то, что распределитель, когда он видит lfence в IDQ, прекращает выделение до тех пор, пока все текущие uops в ROB не уйдут на пенсию. Другими словами, распределитель не будет выделять записи в RS прошлым до lfence пор, пока lfence не lfence. Поскольку тело цикла содержит только 3 других uops, RS с 60 входами никогда не будет заполнен. Фактически, он будет всегда почти пустым.

IDQ в действительности не является простой простой очередью. Он состоит из нескольких аппаратных структур, которые могут работать параллельно. Число микрооперации lfence требует зависит от точного проектирования IDQ. Распределитель, который также состоит из множества различных аппаратных структур, когда это увидеть есть lfence микрооперация в передней части любого из структур IDQ, приостанавливает выделение из этой структуры пока ROB не опустеет. Так что разные uops - usd с различными аппаратными структурами.

UOPS_ISSUED.ANY показывает, что распределитель не выпускает никаких uops в течение примерно 9-10 циклов на итерацию. Что здесь происходит? Ну, одно из lfence состоит в том, что он может рассказать нам, сколько времени требуется, чтобы уволить инструкцию и выделить следующую команду. Для этого можно использовать следующий код сборки:

TIMES T lfence

Счетчики событий производительности не будут работать хорошо при малых значениях T При достаточно большом T и, измеряя UOPS_ISSUED.ANY, мы можем определить, что для увольнения каждого из lfence требуется около 4 циклов. Это потому, что UOPS_ISSUED.ANY будет увеличиваться примерно 4 раза каждые 5 циклов. Таким образом, после каждых 4 циклов распределитель выдает еще один lfence (он не останавливается), затем он ждет еще 4 цикла и так далее. Тем не менее, инструкции, которые приводят к результатам, могут потребовать 1 или несколько циклов для выхода в отставку в зависимости от инструкции. IACA всегда предполагает, что для отмены инструкции требуется 5 циклов.

Наш цикл выглядит следующим образом:

imul eax, eax
lfence
imul edx, edx
dec ecx
jnz .loop

В любом цикле на границе lfence ROB будет содержать следующие инструкции, начиная с верхней части ROB (самая старая инструкция):

imul edx, edx     -  N
dec ecx/jnz .loop -  N
imul eax, eax     -  N+1

Где N обозначает номер цикла, на который была отправлена соответствующая инструкция. Последняя команда, которая будет завершена (дойдет до стадии обратной записи), будет imul eax, eax. и это происходит при цикле N + 4. Счет циклов выдержки распределителя будет увеличен во время циклов, N + 1, N + 2, N + 3 и N + 4. Однако это будет около 5 циклов, пока imul eax, eax уйдет. Кроме того, после того, как он уходит в отставку, распределителю необходимо очистить lfence от IDQ и выделить следующую группу инструкций, прежде чем они смогут быть отправлены в следующем цикле. Выходной сигнал perf говорит нам, что он занимает около 13 циклов на итерацию и что распределитель останавливается (из-за lfence) для 10 из этих 13 циклов.

График из вопроса показывает только число циклов до Т = 100. Однако в этот момент есть еще одно (окончательное) колено. Поэтому было бы лучше построить циклы до T = 120, чтобы увидеть полный шаблон.

Ответ 2

Я думаю, что вы точно измеряете, а объяснение - микроархитектурное, а не какая-либо ошибка измерения.


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

(port1 будет получать edx, eax, empty, edx, eax, empty,... для Skylake 3c latency/1c пропускной способности сразу же, если lfence не блокирует front-end, а накладные расходы не будут масштабироваться с T. )

Вы теряете пропускную способность imul когда только первые шаги из первой цепи находятся в планировщике, потому что front-end еще не пережевывается через imul edx,edx и loop branch. И для того же количества циклов в конце окна, когда трубопровод в основном сбрасывается, и остаются только удаленные от 2-й цепи.


Верхняя дельта выглядит линейной примерно до Т = 60. Я не запускал числа, но наклон до него выглядит разумным для T * 0.25 тактов, чтобы выпустить узкое место в первой цепочке против 3c-latency. т.е. рост дельты может быть 1/12-м так же быстро, как и полный цикл бездействия.

Итак, (учитывая накладные расходы lfence измеренные ниже), при T <60:

no_lfence cycles/iter ~= 3T                  # OoO exec finds all the parallelism
lfence    cycles/iter ~= 3T + T/4 + 9.3      # lfence constant + front-end delay
                delta ~=      T/4 + 9.3

@Margaret сообщает, что T/4 лучше подходит, чем 2*T/4, но я бы ожидал T/4 как в начале, так и в конце, в общей сложности 2T/4 наклон дельты.


После приблизительно T = 60 дельта растет намного быстрее (но все же линейно) с наклоном, равным общему числу циклов бездействия, таким образом, примерно 3c на T. Я думаю, что в этот момент размер планировщика (резервной станции) ограничивая окно вне порядка. Вероятно, вы протестировали на Haswell или Sandybridge/IvyBridge (у которых есть планировщик с 60 входами или 54 входами соответственно. Skylake - 97 записей.

RS отслеживает неиспользуемые команды. Каждая запись RS содержит 1 непроверенный домен uop, который ожидает, что его входы будут готовы, и порт его выполнения, прежде чем он сможет отправить и оставить RS 1.

После lfence из lfence фронт- lfence выходит на 4 за каждый такт, в то время как lfence выполняет 1 раз в 3 такта, выдавая 60 уд в 15 циклов, за это время выполнилось всего 5 команд imul из цепи edx. (Здесь нет нагрузки или хранилища микро-фьюжн, поэтому каждый плагин с объединенным доменом из front-end все еще остается только 1 непроверенным доменом в RS 2.)

Для больших T RS быстро заполняется, и в этот момент интерфейс может только продвинуться вперед со скоростью заднего конца. (Для небольшого T мы lfence до следующей итерации до того, как это произойдет, и это то, что lfence интерфейс). Когда T> RS_size, back-end не может видеть ни одного из uops из цепи eax imul, пока достаточный объемный ход через цепочку edx не edx местом в RS. В этот момент один imul из каждой цепочки может отправлять каждые 3 цикла вместо одной или второй цепочки.

Помните из первого раздела, что время, проведенное только после того, как lfence только первую цепочку = время, прежде чем lfence выполнит только вторую цепочку. Это и здесь.

Мы получаем некоторые из этого эффекта даже без lfence либо lfence при T> RS_size, но есть возможность перекрытия с обеих сторон длинной цепи. ROB, по крайней мере, вдвое больше RS, поэтому окно вне lfence если оно не остановлено lfence должно постоянно поддерживать обе цепи в полете, даже если T несколько больше, чем пропускная способность планировщика. (Помните, что uops покидают RS, как только они будут выполнены. Я не уверен, что это означает, что они должны закончить выполнение и переслать их результат или просто начать выполнение, но это незначительная разница здесь для коротких инструкций ALU. они сделаны, только ROB держит их до тех пор, пока они не уйдут на пенсию, в порядке выполнения программы.)

ROB и регистр файл не должны ограничивать размер окна вне порядка (http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/) в этой гипотетической ситуации или в вашем реальном ситуация. Они должны быть очень большими.


Блокировка front-end представляет собой детальную информацию о lfence на Intel uarch. В руководстве указано, что последующие инструкции не могут выполняться. Эта формулировка позволит lfence выпускать/переименовывать их всех в планировщик (Станция резервирования) и ROB, пока lfence все еще ждет, пока ни один не отправляется в исполнительный блок.

Таким образом, более слабая lfence может иметь плоский верх над T = RS_size, то тот же наклон, который вы видите сейчас для T> 60. (И постоянная часть накладных расходов может быть ниже.)

Обратите внимание, что гарантии на спекулятивное выполнение условных/косвенных ветвей после lfence применяются к выполнению, а не (насколько мне известно) к кодовому изъятию. Простое получение кода-выборки не является (AFAIK) полезным для атаки Spectre или Meltdown. Возможно, временный боковой канал для обнаружения того, как он декодирует, может рассказать вам что-то о выбранном коде...

Я думаю, что AMD LFENCE по крайней мере столь же силен на реальных процессорах AMD, когда соответствующий MSR включен. (Является ли LFENCE сериализация на процессорах AMD?).


Дополнительные lfence накладные расходы:

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

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

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

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

В соответствии с тестированием lfence сама по себе lfence back-to-back lfence " имеет пропускную способность 4 циклов в семействе SnB. Agner Fog сообщает о 2 платных доменах uops (не заработанных), но на Skylake я измеряю его в 6 плавных доменах (все еще не заработанных), если у меня только 1 lfence. Но с большей lfence спиной к спине, это меньше! Вплоть до ~ 2 lfence на каждый lfence со многими спина к спине, что и измеряет Agner.

lfence/dec/jnz (плотная петля без работы) работает на 1 итерации на 10 циклов на SKL, так что это может дать нам представление о реальной дополнительной задержке, которую lfence добавляет к цепочкам lfence даже без front-end и RS-полные узкие места.

Измерение lfence накладных расходов только с одной цепью DEP, ооо Exec нерелевантным:

.loop:
    ;mfence                  ; mfence here:  ~62.3c (with no lfence)
    lfence                   ; lfence here:  ~39.3c
    times 10 imul eax,eax    ; with no lfence: 30.0c
    ; lfence                 ; lfence here:  ~39.6c
    dec   ecx
    jnz   .loop

Без lfence работает в ожидании 30.0c за итератор. С lfence работает на ~ 39.3c за инер, поэтому lfence эффективно добавляет ~ 9.3c "дополнительной задержки" к цепочке отрезков критического пути. (И еще 6 дополнительных флип-доменов).

С lfence после цепи lfence, прямо перед ветвью циклы, она немного медленнее. Но не весь цикл медленнее, так что это указывает на то, что front-end выпускает ветвь loop + и imul в одной группе lfence после того, как lfence позволяет lfence выполнение. Именно так, IDK почему он медленнее. Это не из ветвей промахов.


Получение поведения, которое вы ожидали:

Перемещайте цепи в порядке выполнения программы, например, @BeeOnRope предлагает в комментариях, не требует выполнения вне очереди для использования ILP, так что это довольно тривиально:

.loop:
    lfence      ; at the top of the loop is the lowest-overhead place.

%rep T
    imul   eax,eax
    imul   edx,edx
%endrep

    dec     ecx
    jnz    .loop

Вы можете поставить пары коротких times 8 imul цепочек imul внутри %rep чтобы позволить OoO exec легко провести время.


Сноска 1: Как взаимодействует интерфейс /RS/ROB

Моя ментальная модель заключается в том, что этапы выпуска/переименования/выделения в интерфейсе добавляют новые uops как для RS, так и для ROB одновременно.

Uops покидает RS после выполнения, но оставайтесь в ROB до выхода на пенсию. ROB может быть большим, потому что он никогда не сканировал вне порядка, чтобы найти первый готовый uop, только отсканированный в порядке, чтобы проверить, закончились ли старшие uop и, таким образом, готовы уйти в отставку.

(Я полагаю, что ROB физически является круговым буфером с индексами start/end, а не очередью, которая фактически копирует uops вправо каждый цикл. Но просто подумайте об этом как о очереди/списке с фиксированным максимальным размером, где front-end добавляет uops на фронт, и логика выхода на пенсию удаляет/фиксирует uops с конца, пока они полностью исполняются, вплоть до некоторого предела выхода на пенсию в течение цикла, который обычно не является узким местом, хотя Skylake действительно увеличивал его до 8 за такт для лучшего Hyperthreading, я думаю.)

Uops, такие как nop, xor eax,eax или lfence, которые обрабатываются в интерфейсе (не требуются никакие исполнительные блоки на любых портах), добавляются только к ROB в уже выполненном состоянии. (Кажется, что в записи ROB есть бит, который отмечает, что он готов к отставке, и все еще ждет завершения выполнения. Это состояние, о котором я говорю. Для uops, которым действительно нужен порт выполнения, я предполагаю, что бит ROB установлен через порт завершения от исполнительного блока.)

Uops остается в ROB от выпуска до выхода на пенсию.

Uops остается в RS от выпуска до отправки.


Сноска 2: Сколько RS-записей делает микро-fused uop?

Микро-fused uop выдается на две отдельные записи RS в семействе Sandybridge, но только 1 запись ROB. (Предположим, что перед выпуском он не разламывается, см. Раздел 2.3.5 руководства по оптимизации Intel и режимы микровыключения и адресации. Компактный формат uop в формате Sandybridge не может представлять собой индексированные режимы адресации в ROB во всех случаях. )

Нагрузка может отправляться независимо, перед другим операндом для готовности ALU. (Или для хранилищ с микроплавким доступом, любой из адресов store-address или store-data может отправлять, когда его вход готов, не дожидаясь их обоих.)

Я использовал метод двухдефектных цепочек из вопроса, чтобы экспериментально протестировать это на Skylake (RS size = 97), с микроплавлением or edi, [rdi] против mov + or, и другой отрезной цепью в rsi. (Полный тестовый код, синтаксис NASM на Godbolt)

; loop body
%rep T
%if FUSE
    or edi, [rdi]    ; static buffers are in the low 32 bits of address space, in non-PIE
%else
    mov  eax, [rdi]
    or   edi, eax
%endif
%endrep

%rep T
%if FUSE
    or esi, [rsi]
%else
    mov  eax, [rsi]
    or   esi, eax
%endif
%endrep

Глядя на uops_executed.thread (Незакрепленный-домен) за цикл (или на второе, который perf вычисляет для нас), мы можем видеть количество пропускной способности, которая не зависит от отдельного против сложенного нагрузка.

При малом T (T = 30) все ILP могут быть использованы, и мы получаем ~ 0,67 мкп за такт с микро-слиянием или без него. (Я игнорирую небольшое смещение от 1 дополнительного uop на каждую итерацию цикла от dec/jnz. Это пренебрежимо мало по сравнению с эффектом, который мы увидели бы, если бы микроплавкий только использовал 1 запись RS)

Помните, что load+ or 2 uops, и у нас есть две отвязанные цепи в полете, так что это 4/6, потому что or edi, [rdi] имеет 6-тицитентную задержку. (Не 5, что удивительно, см. Ниже).

При T = 60 у нас все еще есть около 0,66 неиспользованных uops, выполненных за такт для FUSE = 0 и 0,64 для FUSE = 1. Мы все еще можем найти в основном все ILP, но он едва начинает опускаться, поскольку две цепочки депиляции имеют длину 120 мк (по сравнению с RS размером 97).

При T = 120 у нас есть 0,45 незанятых uops за такт для FUSE = 0 и 0,44 для FUSE = 1. Мы определенно прошли мимо колена, но все же находим некоторые из ИЛП.

Если микро-слитый uop принял только 1 вход RS, FUSE = 1 T = 120 должен быть примерно такой же скорости, как FUSE = 0 T = 60, но это не так. Вместо этого FUSE = 0 или 1 практически не имеет разницы при любом T. (Включая более крупные, такие как T = 200: FUSE = 0: 0.395 uops/clock, FUSE = 1: 0.391 uops/clock). Нам нужно идти до очень большого T, прежде чем мы начнем с того времени, когда 1 деп-цепь в полете будет полностью доминировать во времени с 2 в полете и спуститься до 0,33 мк/часов (2/6).

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

Другие странности: общий uops_executed.thread немного ниже для FUSE = 0 при любом заданном T. Как и 2,418,826,591 против 2,419,020,155 при T = 60. Эта разница повторялась до + 60k из 2.4G, достаточно точно. FUSE = 1 медленнее в общих тактовых циклах, но большая часть разницы исходит от более низких частот за такт, а не от более высоких.

Простые режимы адресации, такие как [rdi], должны иметь только 4 задержки цикла, поэтому load + ALU должен быть всего 5 циклов. Но я измеряю задержку в 6 циклов для латентности нагрузки or rdi, [rdi] или с отдельной нагрузкой MOV или с любой другой инструкцией ALU. Я никогда не могу получить часть нагрузки 4c.

Сложный режим адресации, такой как [rdi + rbx + 2064] имеет такую же задержку, когда в цепочке [rdi + rbx + 2064] есть команда ALU, поэтому кажется, что время ожидания Intel 4c для простых режимов адресации применяется только тогда, когда загрузка пересылается в базовый регистр другого (до +0.. 2047 смещения и без индекса).

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


Семейство P6 различно: запись RS содержит флип-домен uop.

@Hadi нашел патент Intel с 2002 года, где на рисунке 12 показан RS в плавленном домене.

Экспериментальное тестирование на Conroe (первый ген Core2Duo, E6600) показывает, что существует большая разница между FUSE = 0 и FUSE = 1 при T = 50. (Размер RS составляет 32 записи).

  • T = 50 FUSE = 1: общее время циклов 2.346G (0.44IPC)
  • T = 50 FUSE = 0: общее время циклов 3.272G (0.62IPC = 0.31 load+ ИЛИ за часы). (perf/ocperf.py не имеет событий для uops_executed на uarches перед Nehalem или так, и у меня нет oprofile установленного на этой машине.)

  • T = 24 существует незначительная разница между FUSE = 0 и FUSE = 1, около 0,47 IPC против 0,9 IPC (~ 0,45 load+ ИЛИ за часы).

T = 24 по-прежнему содержит более 96 байтов кода в цикле, слишком большой для 64-байтового (предварительно декодированного) буфера Core 2, поэтому он не быстрее из-за установки в буфер цикла. Без кэша uop нам нужно беспокоиться об интерфейсе, но я думаю, что все в порядке, потому что я использую только 2-байтные команды с одним-юоном, которые должны легко декодироваться на 4-х разовых консолях за такт.