Понимание влияния 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
Я ожидаю, что процессор будет выполнять imul edx, edx
Последовательность k-й итерации параллельно с imul eax, eax
последовательность следующей (k + 1-й) итерации.
Как то так imul eax, eax
последовательность и D imul edx, edx
один):
|
| 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.
Я хотел бы исправить то, что не так с моим форматом при работе со статическим анализом кода.
2 ответа
Я представлю анализ для случая, когда T = 1 для обоих кодов (с и без lfence
). Затем вы можете расширить это для других значений T. Вы можете обратиться к рисунку 2.4 Руководства по оптимизации Intel для визуального представления.
Поскольку существует только одна легко предсказуемая ветвь, внешний интерфейс будет останавливаться только в том случае, если внутренний конец остановился. В Haswell интерфейс имеет ширину 4, что означает, что из IDQ может быть выдано до 4 слитых мопов (очередь декодирования инструкций, которая является просто очередью, в которой хранятся мупы слитых доменов в порядке, также называемой очередью мопов) резервная станция (RS) входит в планировщик. каждый imul
декодируется в один моп, который не может быть объединен. Инструкции dec ecx
а также jnz .loop
получить макрофьюзинг в интерфейсе к одному мопу. Одно из различий между микрофузией и макрофузией заключается в том, что когда планировщик отправляет макрофизированный моп (который не является микрофизированным) на исполнительный модуль, которому он назначен, он отправляется как один моп. Напротив, микрофузионный моп должен быть разделен на составляющие мопы, каждый из которых должен быть отдельно отправлен в исполнительный модуль. (Тем не менее, расщепление микрофизированных мопов происходит при входе в РС, а не при отправке, см. Сноску 2 в ответе @ Peter). lfence
декодируется в 6 мопов. Распознавание микрофузии имеет значение только в бэкэнде, и в этом случае в петле нет микрофузии.
Поскольку ветвление цикла легко предсказуемо, а количество итераций относительно велико, мы можем просто предположить, не снижая точности, что распределитель всегда сможет выделить 4 мопа за цикл. Другими словами, планировщик будет получать 4 мопа за цикл. Так как нет микрофузии, каждый моп будет отправлен как один моп.
imul
может выполняться только исполняющим модулем Slow Int (см. рисунок 2.4). Это означает, что единственный выбор для выполнения imul
Uops, чтобы отправить их в порт 1. В Haswell Slow Int хорошо конвейеризован, так что один imul
могут быть отправлены за цикл. Но требуется три цикла, чтобы результат умножения был доступен для любой команды, которая требует (этап обратной записи - это третий цикл от этапа отправки конвейера). Таким образом, для каждой цепочки зависимости самое большее imul
можно отправить за 3 цикла.
Так какdec/jnz
предсказано, что принятым, единственное исполнительное устройство, которое может выполнить это, является Первичной ветвью на порту 6.
Таким образом, в любой данный цикл, пока у RS есть место, он получит 4 мопа. Но что за упс? Давайте рассмотрим цикл без lfence:
imul eax, eax
imul edx, edx
dec ecx/jnz .loop (macrofused)
Есть две возможности:
- Два
imul
с одной и той же итерации, одинimul
из соседней итерации, и одинdec/jnz
из одной из этих двух итераций. - Один
dec/jnz
из одной итерации, двеimul
с следующей итерации, и одинdec/jnz
из той же итерации.
Таким образом, в начале любого цикла РС получит хотя бы один dec/jnz
и хотя бы один imul
из каждой цепочки. В то же время, в том же цикле и из тех мопов, которые уже есть в RS, планировщик выполнит одно из двух действий:
- Отправьте самый старый
dec/jnz
в порт 6 и отправить самую старуюimul
это готово к порту 1. Это в общей сложности 2 моп. - Поскольку Slow Int имеет задержку 3 цикла, но есть только две цепи, для каждого цикла из 3 циклов нет
imul
в РС будет готов к исполнению. Тем не менее, всегда есть хотя бы одинdec/jnz
в РС. Так что планировщик может отправить это. Это всего 1 моп.
Теперь мы можем рассчитать ожидаемое количество мопов в RS, XN, в конце любого данного цикла N:
XN = XN-1 + (количество мопов, которое должно быть выделено в RS в начале цикла N) - (ожидаемое количество мопов, которое будет отправлено в начале цикла N)
= XN-1 + 4 - ((0 + 1) * 1/3 + (1 + 1) * 2/3)
= XN-1 + 12/3 - 5/3
= XN-1 + 7/3 для всех N > 0
Начальное условие для повторения - X0 = 4. Это простое повторение, которое можно решить, развернув XN-1.
XN = 4 + 2,3 * N для всех N >= 0
РС в Haswell имеет 60 записей. Мы можем определить первый цикл, в котором ожидается заполнение RS:
60 = 4 + 7/3 * N
N = 56 / 2,3 = 24,3
Таким образом, в конце цикла 24.3 ожидается, что RS будет заполнен. Это означает, что в начале цикла 25.3 RS не сможет получить никаких новых мопов. Теперь количество итераций, которое я, рассматриваю, определяет, как вам следует приступить к анализу. Поскольку для цепочки зависимостей потребуется не менее 3*I циклов, для достижения цикла 24,3 требуется около 8,1 итераций. Так что, если число итераций больше, чем 8.1, как в данном случае, вам нужно проанализировать, что происходит после цикла 24.3.
Планировщик отправляет инструкции со следующими скоростями каждый цикл (как обсуждалось выше):
1
2
2
1
2
2
1
2
.
.
Но распределитель не будет распределять какие-либо мопы в RS, если не будет по крайней мере 4 доступных записей. В противном случае он не будет тратить энергию на выдачу мопов с неоптимальной пропускной способностью. Однако только в начале каждого 4-го цикла в РС есть как минимум 4 свободных записи. Таким образом, начиная с цикла 24.3, ожидается, что распределитель остановится 3 из каждых 4 циклов.
Другим важным наблюдением для анализируемого кода является то, что никогда не бывает, что может быть отправлено более 4 мопов, что означает, что среднее количество мопов, которые покидают свои исполнительные единицы за цикл, не больше 4. Максимум 4 мопа может быть удален из буфера повторного заказа (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 инструкции могут быть удалены. Таким образом, МПК составляет 4/3 = 1,3, а ИПЦ 3/4 = 0,75. Это намного меньше, чем теоретический оптимальный IPC 4 (даже без учета микро- и макро-синтеза). Поскольку выход на пенсию происходит по порядку, поведение при выходе на пенсию будет таким же.
Мы можем проверить наш анализ, используя оба perf
и МАКА. Я буду обсуждать 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
может использоваться для подсчета количества мопов, выданных RS, и количества циклов, в которых был остановлен распределитель (без конкретной причины). Первый простой (не показан в perf
выход); 1 миллион * 3 = 3 миллиона + небольшой шум. Последнее гораздо интереснее. Это показывает, что около 73% всего времени распределитель останавливался из-за полной RS, что соответствует нашему анализу. RESOURCE_STALLS.RS
подсчитывает количество циклов, в которых распределитель был остановлен из-за полного RS. Это близко к UOPS_ISSUED.ANY
потому что распределитель не останавливается по какой-либо другой причине (хотя по какой-то причине разница может быть пропорциональна количеству итераций, мне нужно будет увидеть результаты для T>1).
Анализ кода без lfence
может быть расширен, чтобы определить, что произойдет, если lfence
был добавлен между двумя imul
s. Давайте проверим perf
результаты в первую очередь (IACA, к сожалению, не поддерживает 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
не будет выполнять инструкции быстрее, поэтому RESOURCE_STALLS.ROB
не должно меняться. UOPS_ISSUED.ANY
а также RESOURCE_STALLS.RS
особенно интересны В этом выводе UOPS_ISSUED.ANY
считает циклы, а не упс. Количество мопов также можно посчитать (используя 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 мопов за итерацию (без слияния). Это означает, что lfence
это было помещено между двумя imul
S был расшифрован в 6 моп. Вопрос на миллион долларов теперь в том, что делают эти мопы и как они передвигаются в трубе.
RESOURCE_STALLS.RS
это ноль. Что это значит? Это указывает на то, что распределитель, когда он видит lfence
в IDQ он прекращает распределяться до тех пор, пока все текущие мопы в ROB не будут удалены. Другими словами, распределитель не будет распределять записи в RS после lfence
до lfence
уходит в отставку. Поскольку тело цикла содержит только 3 других мопа, RS с 60 записями никогда не будет заполнен. На самом деле, он всегда будет почти пустым.
На самом деле IDQ - это не одна простая очередь. Он состоит из нескольких аппаратных структур, которые могут работать параллельно. Количество мопсов lfence
Требуется зависит от точного дизайна IDQ. Распределитель, который также состоит из множества различных аппаратных структур, когда он видит lfence
В начале любой из структур IDQ, он приостанавливает выделение из этой структуры, пока ROB не опустеет. Так что разные мопы используются с разными аппаратными структурами.
UOPS_ISSUED.ANY
показывает, что распределитель не выдает никаких мопов в течение примерно 9-10 циклов за итерацию. Что здесь происходит? Ну, одно из применений lfence
является то, что он может сказать нам, сколько времени требуется, чтобы удалить инструкцию и выделить следующую инструкцию. Для этого можно использовать следующий код сборки:
TIMES T lfence
Счетчики событий производительности не будут хорошо работать для малых значений T
, Для достаточно большого Т и путем измерения UOPS_ISSUED.ANY
мы можем определить, что для выхода на пенсию требуется около 4 циклов. lfence
, Это потому что 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 циклов.
График из вопроса показывает только количество циклов до T=100. Тем не менее, есть еще одно (последнее) колено в этой точке. Поэтому было бы лучше построить циклы до T=120, чтобы увидеть полную картину.
Я думаю, что вы измеряете точно, и объяснение микроархитектурное, а не какая-либо ошибка измерения.
Я думаю, что ваши результаты для среднего и низкого уровня T подтверждают вывод, что lfence
останавливает интерфейс даже от выдачи мимо lfence
пока все более ранние инструкции не будут удалены, вместо того, чтобы все мопы из обеих цепей уже были выпущены и просто ждали lfence
щелкнуть выключателем, и пусть умножения из каждой цепочки начнут отправляться на чередующихся циклах.
(port1 получит edx,eax,empty,edx,eax,empty,... для множителя пропускной способности 3c задержки / 1c Skylake сразу, если lfence
не блокировал входную часть, и накладные расходы не масштабировались бы с T.)
Ты теряешь imul
пропускной способности, когда в планировщике присутствуют только мопы из первой цепочки, потому что интерфейс не пережевал imul edx,edx
и петля ветвится пока. И для того же числа циклов в конце окна, когда конвейер в основном дренирован и остались только мопы из 2-й цепочки.
Верхняя дельта выглядит линейной примерно до T=60. Я не управлял номерами, но уклон там кажется разумным для T * 0.25
часы для выдачи первой цепочки против узкого места выполнения 3c-латентности. т. е. дельта может расти на 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
@ Маргарет сообщает, что T/4
лучше подходит, чем 2*T / 4
, но я бы ожидал T/4 как в начале, так и в конце, в общей сложности 2T/4 склона дельты.
Приблизительно после T=60, дельта растет намного быстрее (но все еще линейно), с наклоном, примерно равным полному циклу отсутствия защиты, таким образом, приблизительно 3c на T. Я думаю, что в этот момент размер планировщика (Reservation Station) ограничение окна не в порядке. Вы, вероятно, тестировали на Haswell или Sandybridge/IvyBridge ( которые имеют планировщик на 60 или 54 входа соответственно. Skylake имеет 97 записей).
RS отслеживает невыполненные мопы. Каждая запись RS содержит 1 uop неиспользуемого домена, который ожидает готовности своих входов, и свой порт выполнения, прежде чем он сможет отправлять и покинуть RS 1.
После lfence
передний конец выдает по 4 за такт, в то время как сервер выполняет по 1 на 3 такта, выдавая 60 мопов за ~15 циклов, в течение которых только 5 imul
инструкции от edx
цепь выполнила. (Здесь нет никакой микросинтеграции нагрузки или хранилища, поэтому каждый моп с объединенным доменом с внешнего интерфейса все еще только 1 моп с незадействованным доменом в RS 2.)
При больших T RS быстро заполняется, и в этот момент передний конец может прогрессировать только со скоростью внутреннего конца. (Для маленьких T мы получаем следующую итерацию lfence
до того, как это произойдет, и это то, что останавливает интерфейс). Когда T> RS_size, сервер не может видеть ни одного из мопов из eax
Имуль цепи до тех пор, пока не будет достаточно продвижение через edx
Сеть освободила место в РС. В этот момент один imul
Из каждой цепочки можно отправлять каждые 3 цикла, а не только 1-ю или 2-ю цепочку.
Помните из первого раздела, что время, проведенное сразу после lfence
только выполнение первой цепочки = время как раз перед lfence
выполнение только второй цепочки. Это применимо и здесь.
Мы получаем некоторые из этого эффекта даже без lfence
, для T> RS_size, но есть возможность перекрытия с обеих сторон длинной цепочки. ROB по крайней мере в два раза больше RS, поэтому окно не в порядке, когда не остановлено lfence
должен быть в состоянии поддерживать обе цепи в полете постоянно, даже когда T несколько больше, чем пропускная способность планировщика. (Помните, что мопы покидают RS, как только они выполнятся. Я не уверен, означает ли это, что они должны завершить выполнение и переслать свой результат, или просто начать выполнение, но здесь есть небольшая разница для коротких инструкций ALU. они сделаны, только РОБ держит их, пока они не уйдут в отставку, в программном порядке.)
ROB и файл реестра не должны ограничивать размер окна не по порядку ( http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/) в этой гипотетической ситуации или в вашей реальной ситуации. ситуация. Они оба должны быть достаточно большими.
Блокировка внешнего интерфейса - это деталь реализации lfence
на кортах Intel. В руководстве только сказано, что более поздние инструкции не могут быть выполнены. Эта формулировка позволит внешнему интерфейсу выпускать / переименовывать их в планировщик (Станция резервирования) и ROB при lfence
все еще ждет, пока никто не отправляется в исполнительный модуль.
Так слабее lfence
может иметь плоские накладные расходы до T=RS_size, тогда тот же наклон, который вы видите сейчас для T> 60. (И постоянная часть накладных расходов может быть ниже.)
Обратите внимание, что гарантии о спекулятивном исполнении условных / косвенных веток после lfence
применять к выполнению, а не (насколько я знаю) для выборки кода. Простое инициирование выборки кода (AFAIK) бесполезно для атаки Spectre или Meltdown. Возможно, побочный канал синхронизации для определения того, как он декодирует, может рассказать вам кое-что о извлеченном коде...
Я думаю, что LFENCE AMD, по крайней мере, так же силен на реальных процессорах AMD, когда соответствующая MSR включена. ( Есть ли сериализация LFENCE на процессорах AMD?).
дополнительный lfence
накладные расходы:
Ваши результаты интересны, но меня совсем не удивляет, что от lfence
сам (для малого T), а также компонент, который масштабируется с T.
Помни что lfence
не позволяет запускать более поздние инструкции до тех пор, пока более ранние инструкции не будут удалены. Вероятно, это, по крайней мере, на пару циклов / этапов конвейера позже, чем когда их результаты готовы для обхода других модулей выполнения (т. Е. С нормальной задержкой).
Так что для малого T, безусловно, важно добавить дополнительную задержку в цепочку, требуя, чтобы результат не только был готов, но и записан обратно в файл реестра.
Это, вероятно, занимает дополнительный цикл или около того для lfence
разрешить этапу выпуска / переименования снова начать работать после обнаружения отмены последней инструкции перед ним. Процесс выпуска / переименования занимает несколько этапов (циклов) и, возможно, блоки lfence в начале этого процесса, а не на самом последнем этапе перед добавлением мопов в часть OoO ядра.
Даже спина к спине lfence
сам по себе тестирование Agner Fog имеет 4-тактную пропускную способность на семействе SnB. Agner Fog сообщает о 2 мопах слитых доменов (без слияния), но на Skylake я измеряю их на 6 слитых доменах (пока без слияния), если у меня только 1 lfence
, Но с большим lfence
спина к спине, это меньше мопс! До ~2 моп в lfence
со многими спина к спине, как это измеряет Агнер.
lfence
/ dec
/ jnz
(плотный цикл без работы) выполняется в 1 итерации на ~10 циклов в SKL, так что это может дать нам представление о реальной дополнительной задержке, которая lfence
добавляет к цепям dep даже без входного и RS-full узких мест.
Измерение lfence
накладные расходы только с одной цепочкой депо, OoO 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,3 с на итера, так lfence
эффективно добавили ~9.3c "дополнительной задержки" в цепочку критических путей. (И 6 дополнительных мопов слитых доменов).
С lfence
после цепочки imul, прямо перед ветвью петли, она немного медленнее. Но не на целый цикл медленнее, так что это указывало бы на то, что внешний интерфейс выдает loop-branch + и imul в одной группе вопросов после 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
цепи внутри %rep
позволить OoO exec легко провести время.
Сноска 1: Как взаимодействуют интерфейс / RS / ROB
Моя ментальная модель заключается в том, что этапы выдачи / переименования / выделения во внешнем интерфейсе добавляют новые мопы как для RS, так и для ROB одновременно.
Uops покидают RS после выполнения, но остаются в ROB до упорядоченного выхода на пенсию. ROB может быть большим, потому что он никогда не сканируется не по порядку, чтобы найти первый готовый моп, только отсканированный, чтобы проверить, закончили ли самые старые мопы выполнение и, таким образом, готовы удалиться.
(Я предполагаю, что ROB физически представляет собой циклический буфер с индексами начала / конца, а не очередь, которая фактически копирует мопы вправо каждый цикл. Но просто представьте, что это очередь / список с фиксированным максимальным размером, где интерфейс добавляет мопы впереди, и логика выхода на пенсию удаляет / передает мопы с конца до тех пор, пока они полностью выполняются, до некоторого предела выхода на пенсию за цикл, который обычно не является узким местом, хотя Skylake действительно увеличивал его до 8 за такт для лучшего Hyperthreading, я думаю.)
Упс, как nop
, xor eax,eax
, или же lfence
, которые обрабатываются во внешнем интерфейсе (не требуются никакие исполнительные блоки на каких-либо портах), добавляются только в ROB в уже выполненном состоянии. (Предполагается, что в записи ROB есть бит, который помечает ее как готовую к удалению, а еще ожидает завершения выполнения. Это состояние, о котором я говорю. Для мопов, которым действительно нужен порт выполнения, я предполагаю, что бит ROB установлен через порт завершения из исполнительного блока.)
Uops остаются в ROB от выпуска до выхода на пенсию.
Упс остается в РС от выдачи до отправки.
Сноска 2: Сколько записей RS занимает микроплавкий моп?
Микроплавленый моп выдан двум отдельным записям RS в семействе Sandybridge, но только 1 записи ROB. (Предполагая, что он не является ламинированным перед выпуском, см. Раздел 2.3.5 руководства по оптимизации Intel и режимы Micro fusion и адресации. Более компактный формат uop семейства Sandybridge не может представлять индексированные режимы адресации в ROB во всех случаях.)
Загрузка может отправляться независимо, опережая другой операнд для готовности ALU. (Или для хранилищ с микросхемой, либо адрес магазина, либо данные хранилища могут отправлять, когда его вход готов, без ожидания обоих.)
Я использовал метод с двумя цепочками из этого вопроса, чтобы экспериментально проверить это на Skylake (размер RS = 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 дополнительный моп за итерацию цикла от dec/jnz. Это незначительно по сравнению с тем эффектом, который мы увидим, если в микросопливных мопах используется только 1 запись RS)
Помните, что нагрузка + or
2 мопа, и у нас есть 2 деп цепи в полете, так что это 4/6, потому что or edi, [rdi]
имеет задержку 6 циклов. (Не 5, что удивительно, см. Ниже.)
При T = 60 у нас все еще остается около 0,66 неиспользованных мопов за такт для FUSE=0 и 0,64 для FUSE=1. Мы все еще можем найти в основном весь ILP, но он едва начинает падать, так как две цепочки dep имеют длину 120 мопов (по сравнению с размером RS 97).
При T=120 мы имеем 0,45 неиспользованных мопов за такт для FUSE=0 и 0,44 для FUSE=1. Мы определенно прошли колено здесь, но все еще находим часть ILP.
Если микроплавкий переход занимал только 1 вход RS, FUSE=1 T=120 должен быть примерно такой же скорости, как FUSE=0 T=60, но это не так. Вместо этого FUSE=0 или 1 практически не имеет значения при любом T. (Включая более крупные, такие как T=200: FUSE=0: 0,395 моп / такт, FUSE=1: 0,391 моп / такт). Нам пришлось бы перейти к очень большому Т, прежде чем мы начнем время с 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 цикла, поэтому нагрузка + ALU должна составлять только 5 циклов. Но я измеряю задержку 6 циклов для задержки использования нагрузки or rdi, [rdi]
или с отдельной MOV-загрузкой, или с любой другой инструкцией ALU, я никогда не могу получить нагрузочную часть равной 4 c.
Сложный режим адресации, такой как [rdi + rbx + 2064]
имеет ту же задержку, когда в цепочке dep есть инструкция ALU, поэтому кажется, что задержка Intel 4 c для простых режимов адресации применяется только тогда, когда нагрузка пересылается в базовый регистр другой нагрузки (с смещением до +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,346 Гц (0,44IPC)
T = 50 FUSE=0: общее время 3,272G циклов (0,62IPC = 0,31 нагрузки + ИЛИ за такт). (
perf
/ocperf.py
не имеет событий дляuops_executed
на Uarches до Nehalem или около того, и у меня нетoprofile
установлен на этой машине.)При T=24 разница между FUSE=0 и FUSE = 1 незначительна, около 0,47 IPC против 0,9 IPC (~0,45 нагрузки + ИЛИ за такт).
T=24 по-прежнему превышает 96 байт кода в цикле, слишком большой для 64-байтового (до декодирования) буфера цикла Core 2, поэтому он не быстрее из-за размещения в буфере цикла. Без uop-кеша нам нужно беспокоиться о внешнем интерфейсе, но я думаю, что у нас все хорошо, потому что я использую исключительно 2-байтовые инструкции с одним мопом, которые должны легко декодироваться с 4 мопами слитых доменов за такт.