Понимание влияния 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
Как можно объяснить значения Cycles:u lfence
?
Я бы ожидал, что они будут похожи на те, что у Cycles:u no lfence
так как один lfence
должен предотвращать параллельную реализацию первой итерации для двух блоков.
Я не думаю, что это из - за lfence
накладных расходов, поскольку я считаю, что должно быть постоянным для всех T
s.
Я хотел бы исправить то, что неправильно с моим forma mentis, когда речь идет о статическом анализе кода.
Поддержка репозитория с исходными файлами.
Ответы
Ответ 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-х разовых консолях за такт.