Странные эффекты производительности от соседних зависимых хранилищ в цикле погони за указателями на IvyBridge. Добавление дополнительной нагрузки ускоряет это? - PullRequest
0 голосов
/ 08 января 2019

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

section .bss
align   64
buf:    resb    64

section .text
global _start
_start:
    mov rcx,         1000000000
    mov qword [buf], buf
    mov rax,         buf
loop:
    ; I will insert payload here
    ; as is described below 

    dec rcx
    jne loop

    xor rdi,    rdi
    mov rax,    60
    syscall

вариант 1:

Я вставляю в расположение полезной нагрузки:

mov qword [rax+8],  8
mov rax,            [rax]

perf показывает, что цикл равен 5,4 с / итера. Это несколько понятно, потому что задержка L1d равна 4 циклам.

вариант 2:

Я изменяю порядок этих двух инструкций:

mov rax,            [rax]
mov qword [rax+8],  8

Результат внезапно становится 9c / iter. Я не понимаю почему. Поскольку первая инструкция следующей итерации не зависит от второй инструкции текущей итерации, этот параметр не должен отличаться от случая 1.

Я также использовал инструмент IACA для статического анализа этих двух случаев, но инструмент ненадежен, потому что он предсказывает один и тот же результат 5.71c / iter для обоих случаев, что противоречит эксперименту.

дело 3:

Затем я вставляю несущественную mov инструкцию для случая 2:

mov rax,            [rax]
mov qword [rax+8],  8
mov rbx,            [rax+16] 

Теперь результат становится 6.8c / iter. Но как может нерелевантный mov вставить повысить скорость с 9c / iter до 6.8c / iter?

Инструмент IACA предсказывает неверный результат, как и в предыдущем случае, он показывает 5,24c / iter.

Я сейчас совершенно запутался, как понять приведенные выше результаты?

Изменить для получения дополнительной информации:

В случае 1 и 2 есть адрес rax+8. Те же самые результаты остаются для случаев 1 и 2, если rax+8 изменяется на rax+16 или rax+24. Но что-то удивительное случается, когда оно меняется на rax+32: случай 1 становится 5,3 с / iter, случай 2 внезапно становится 4,2 с / iter.

Изменить для более perf событий:

$ perf stat -ecycles,ld_blocks_partial.address_alias,int_misc.recovery_cycles,machine_clears.count,uops_executed.stall_cycles,resource_stalls.any ./a.out

кейс 1 для [rax+8]:

 5,429,070,287      cycles                                                        (66.53%)
         6,941      ld_blocks_partial.address_alias                                     (66.75%)
       426,528      int_misc.recovery_cycles                                      (66.83%)
        17,117      machine_clears.count                                          (66.84%)
 2,182,476,446      uops_executed.stall_cycles                                     (66.63%)
 4,386,210,668      resource_stalls.any                                           (66.41%)

дело 2 для [rax+8]:

 9,018,343,290      cycles                                                        (66.59%)
         8,266      ld_blocks_partial.address_alias                                     (66.73%)
       377,824      int_misc.recovery_cycles                                      (66.76%)
        10,159      machine_clears.count                                          (66.76%)
 7,010,861,225      uops_executed.stall_cycles                                     (66.65%)
 7,993,995,420      resource_stalls.any                                           (66.51%)

кейс 3 для [rax+8]:

 6,810,946,768      cycles                                                        (66.69%)
         1,641      ld_blocks_partial.address_alias                                     (66.73%)
       223,062      int_misc.recovery_cycles                                      (66.73%)
         7,349      machine_clears.count                                          (66.74%)
 3,618,236,557      uops_executed.stall_cycles                                     (66.58%)
 5,777,653,144      resource_stalls.any                                           (66.53%)

кейс 2 для [rax+32]:

 4,202,233,246      cycles                                                        (66.68%)
         2,969      ld_blocks_partial.address_alias                                     (66.68%)
       149,308      int_misc.recovery_cycles                                      (66.68%)
         4,522      machine_clears.count                                          (66.68%)
 1,202,497,606      uops_executed.stall_cycles                                     (66.64%)
 3,179,044,737      resource_stalls.any                                           (66.64%)

1 Ответ

0 голосов
/ 21 января 2019

Tl; DR: В этих трех случаях при выполнении загрузки и хранения одновременно налагается штраф в несколько циклов. Задержка загрузки находится на критическом пути во всех трех случаях, но штраф в разных случаях различен. Случай 3 примерно на один цикл выше, чем случай 1 из-за дополнительной нагрузки.


Метод анализа 1: Использование событий производительности сваливания

Мне удалось воспроизвести ваши результаты по всем трем случаям на IvB и SnB. Числа, которые я получил, находятся в пределах 2% от ваших чисел. Число циклов, необходимое для выполнения одной итерации в случаях 1, 2 и 4, составляет 5,4, 8,9 и 6,6 соответственно.

Давайте начнем с внешнего интерфейса. События производительности LSD.CYCLES_4_UOPS и LSD.CYCLES_3_UOPS показывают, что в основном все мопы выдаются из LSD. Кроме того, эти события вместе с LSD.CYCLES_ACTIVE показывают, что в каждом цикле, в котором ЛСД не останавливается, выдается 3 мопа в случаях 1, а в случае 3 выдается 4 мопа. Другими словами, как и ожидалось, мопы каждой итерации выпускаются вместе в одной группе в одном цикле.

Во всех следующих отношениях знак "= ~" означает, что разница находится в пределах 2%. Начну со следующего эмпирического наблюдения:

UOPS_ISSUED.STALL_CYCLES + LSD.CYCLES_ACTIVE = ~ cycles

Обратите внимание, что количество LSD-событий на SnB необходимо корректировать, как описано в здесь .

У нас также есть следующие отношения:

case 1: UOPS_ISSUED.STALL_CYCLES = ~ RESOURCE_STALLS.ANY = ~ 4.4c / iter
дело 2: UOPS_ISSUED.STALL_CYCLES = ~ RESOURCE_STALLS.ANY = ~ 7,9c / iter
дело 3: UOPS_ISSUED.STALL_CYCLES = ~ RESOURCE_STALLS.ANY = ~ 5,6 с / итера

Это означает, что причина проблемы в том, что один или несколько необходимых ресурсов в бэкэнде недоступны. Таким образом, мы можем с уверенностью исключить весь интерфейс из рассмотрения. В случаях 1 и 2 этим ресурсом является RS. В случае 3 киоски из-за RS составляют около 20% всех киосков ресурсов 1 .

Давайте теперь сосредоточимся на случае 1. Всего имеется 4 неиспользуемых мопа домена: 1 загрузочный моп, 1 STA, 1 STD и 1 dec / jne. Нагрузка и STA-мопы зависят от предыдущего. Всякий раз, когда LSD выдает группу мопов, STD и мопы прыжка могут быть отправлены в следующем цикле, поэтому следующий цикл не вызовет событие остановки выполнения. Однако самая ранняя точка, в которой могут быть отправлены загрузки и STA-мопы, находится в том же цикле, в котором результат загрузки записывается обратно. Корреляция между CYCLES_NO_EXECUTE и STALLS_LDM_PENDING указывает, что причина, по которой не было бы готовых к выполнению мопов, состоит в том, что все мопы, которые находятся в RS, ожидают L1 для обслуживания ожидающих запросов загрузки. В частности, половина мопов в RS является ментами загрузки, а другая половина - STA, и все они ожидают завершения загрузки соответствующей предыдущей итерации. LSD.CYCLES_3_UOPS показывает, что LSD ожидает, пока в RS не будет по крайней мере 4 свободных записей, только тогда он выдает группу мопов, которые составляют полную итерацию. В следующем цикле два из этих мопов будут отправлены, тем самым освобождая 2 записи RS 2 . Другим придется ждать загрузки, от которой они зависят, чтобы завершить. Скорее всего, загрузка завершена в программном порядке. Поэтому LSD ожидает, пока STA и не загрузит мопы самой старой итерации, которая еще должна быть выполнена, покинуть RS. Таким образом, UOPS_ISSUED.STALL_CYCLES + 1 = ~ средняя задержка загрузки 3 . Можно сделать вывод, что средняя задержка загрузки в случае 1 составляет 5,4 с. Большая часть этого относится к случаю 2, за исключением одного различия, как я объясню позже.

Поскольку мопы в каждой итерации образуют цепочку зависимостей, у нас также есть:

cycles = ~ средняя задержка загрузки.

Таким образом:

cycles = ~ UOPS_ISSUED.STALL_CYCLES + 1 = ~ средняя задержка загрузки.

В случае 1 средняя задержка загрузки составляет 5,4 с. Мы знаем, что в лучшем случае задержка кэша L1 равна 4c, поэтому штраф за задержку загрузки составляет 1.4c. Но почему эффективная задержка загрузки не равна 4с?

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

Загрузка и STA-мопы могут быть отправлены только на порт 2 или 3. События UOPS_EXECUTED_PORT.PORT_2 и UOPS_EXECUTED_PORT.PORT_3 могут использоваться для подсчета количества мопов, отправленных на порты 2 и 3 соответственно.

дело 1: UOPS_EXECUTED_PORT.PORT_2 + UOPS_EXECUTED_PORT.PORT_3 = ~ 2 моп / итер
дело 2: UOPS_EXECUTED_PORT.PORT_2 + UOPS_EXECUTED_PORT.PORT_3 = ~ 6 моп / итер
случай 3: UOPS_EXECUTED_PORT.PORT_2 + UOPS_EXECUTED_PORT.PORT_3 = ~ 4,2 моп / итэр

В случае 1 общее количество отправленных моп AGU точно равно количеству удаленных моп AGU; нет повторов. Так что планировщик никогда не прогнозирует. В случае 2, в среднем, 2 повторных воспроизведения для каждой операции AGU, а это означает, что планировщик в среднем дважды прогнозирует ошибочные действия для каждой операции AGU. Почему в случае 2 есть неправильные прогнозы, а в случае 1 - нет?

Планировщик будет воспроизводить моп в зависимости от нагрузки по любой из следующих причин:

  • Отсутствует кэш L1.
  • Неправильное предсказание неоднозначности памяти.
  • Нарушение целостности памяти.
  • Удар кеша L1, но есть трафик L1-L2.
  • Ошибочное прогнозирование номера виртуальной страницы.
  • Некоторые другие (недокументированные) причины.

Первые 5 причин могут быть окончательно исключены с помощью соответствующих событий производительности. Патрик Фэй (Intel) говорит следующее:

Наконец, да, при переключении между загрузить и магазин. Мне сказали не быть более конкретным, чем «несколько».
...
SNB может читать и писать разные банки в одном цикле.

Я нахожу эти утверждения, возможно, намеренно, немного двусмысленными. Первое утверждение предполагает, что загрузка и сохранение в L1 никогда не могут полностью перекрываться. Второй предполагает, что загрузка и хранение могут быть выполнены в одном и том же цикле, только если они находятся в разных банках. Хотя нахождение в разных банках не может быть ни необходимым, ни достаточным условием. Но одно можно сказать наверняка: если есть параллельные запросы на загрузку и сохранение, загрузка (и хранилище) может быть отложена на один или несколько циклов. Это объясняет средний штраф 1.4c на задержку загрузки в случае 1.

Существует разница между случаем 1 и случаем 2. В случае 1 STA и нагрузочные мопы, которые зависят от одного и того же нагрузочного мопа, вырабатываются вместе в одном и том же цикле. С другой стороны, в случае 2 STA и загрузки uops, которые зависят от одной и той же загрузки нагрузки, принадлежат двум разным группам проблем. Время простоя проблемы на итерацию будет по существу равно времени, которое требуется для последовательного выполнения одной загрузки и вывода из эксплуатации одного хранилища. Вклад каждой операции можно оценить, используя CYCLE_ACTIVITY.STALLS_LDM_PENDING. Выполнение STA-операции занимает один цикл, поэтому хранилище может завершить работу в цикле, который следует непосредственно за циклом, в котором отправляется STA.

Средняя задержка нагрузки составляет CYCLE_ACTIVITY.STALLS_LDM_PENDING + 1 цикл (цикл, в котором отправляется нагрузка) + 1 цикл (цикл, в котором отправляется скачок). Нам нужно добавить 2 цикла к CYCLE_ACTIVITY.STALLS_LDM_PENDING, потому что в этих циклах нет остановок выполнения, но они составляют часть общей задержки загрузки. Это равно 6,8 + 2 = 8,8 циклов = ~ cycles.

Во время выполнения первых дюжин (или около того) итераций прыжок и моп STD будут выделяться в RS каждый цикл. Они всегда будут отправлены для выполнения в цикле, который следует за циклом выпуска. В какой-то момент RS заполнится, и все записи, которые еще не были отправлены, будут STA и загрузят мопы, которые ожидают завершения загрузочных мопов соответствующих предыдущих итераций (запишут их результаты). Таким образом, распределитель останавливается до тех пор, пока не будет достаточно свободных записей RS, чтобы выполнить целую итерацию. Давайте предположим, что самый старый загрузочный uop записал свой результат в цикле T + 0. Я буду ссылаться на итерацию, которой принадлежит этот загрузочный uop, в качестве текущей итерации. Произойдет следующая последовательность событий:

В цикле T + 0: отправка STA-изменения текущей итерации и загрузки-загрузки следующей итерации. В этом цикле нет распределения, потому что недостаточно записей RS. Этот цикл считается как цикл остановки выделения, но не как цикл остановки выполнения.

В цикле T + 1: STA-моп завершает выполнение, и хранилище удаляется. Распределение выполняется в следующей следующей итерации. Этот цикл считается как цикл остановки выполнения, но не как цикл остановки выделения.

В цикле T + 2: только что выделенные прыжки и STD-мопы были отправлены. Этот цикл считается как цикл остановки выделения, но не как цикл остановки выполнения.

В циклах T + 3 до T + 3 + CYCLE_ACTIVITY.STALLS_LDM_PENDING - 2: Все эти циклы учитываются как циклы задержки выполнения и выделения. Обратите внимание, что здесь есть CYCLE_ACTIVITY.STALLS_LDM_PENDING - 1 цикл.

Следовательно, UOPS_ISSUED.STALL_CYCLES должно быть равно 1 + 0 + 1 + CYCLE_ACTIVITY.STALLS_LDM_PENDING - 1. Давайте проверим: 7,9 = 1 + 0 + 1 + 6,8-1.

Исходя из рассуждений по случаю 1, cycles должно быть равно UOPS_ISSUED.STALL_CYCLES + 1 = 7,9 + 1 = ~ фактическое измеренное значение cycles. Штраф, понесенный при одновременной загрузке и хранении, на 3,6 с выше, чем в случае 1. Это как если бы нагрузка ожидала принятия хранилища. Я думаю, это также объясняет, почему есть повторы в случае 2, но не в случае 1.

В случае 3: 1 STD, 1 STA, 2 нагрузки и 1 скачок. Все мопы одной итерации могут быть распределены за один цикл, потому что пропускная способность IDQ-RS составляет 4 слитых мопа за цикл. Мопс становится неприкосновенным на входе в РС. Для 1 STD требуется 1 цикл. Прыжок также занимает 1 цикл. Существует три порта AGU, но только 2 порта AGU. Таким образом, требуется 2 цикла (по сравнению с 1 в случае 1 и 2) для отправки мопов AGU. Отправленная группа AGU будет одной из следующих:

  • Вторая загрузка uop и STA uop той же итерации. Они зависят от первой загрузки uop той же итерации. Используются оба порта AGU.
  • Первая загрузка в следующей итерации может быть отправлена ​​в следующем цикле. Это зависит от загрузки предыдущей итерации. Используется только один из двух портов AGU.

Поскольку для освобождения достаточного количества записей RS для размещения всей группы вопросов требуется еще один цикл, UOPS_ISSUED.STALL_CYCLES + 1 - 1 = UOPS_ISSUED.STALL_CYCLES = ~ средняя задержка загрузки = ~ 5,6 с, что очень близко к Случай 1. Штраф составляет около 1.6с. Это объясняет, почему в случае 3 по сравнению со случаями 1 и 2 каждая мера AGU отправляется в среднем в 1,4 раза.

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

cycles = ~ средняя задержка загрузки + 1 = 6,6 с / мегатерна, что в действительности точно соответствует cycles, как измерено в моей системе.

Подробный анализ, аналогичный анализу для случая 2, можно провести и для случая 3. В случае 3 выполнение STA перекрывается с задержкой второй загрузки. Задержки обеих нагрузок также в основном перекрываются.

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


Сноска

(1) Остальные 80% времени тратятся на остановку матрицы нагрузки. Эта структура едва упоминается в руководстве. Он используется для указания зависимостей между мопами и ментами загрузки. приблизительно иметь 32 записи на SnB и IvB. Не существует задокументированного события производительности, которое могло бы подсчитывать количество остановок на LM. Все задокументированные события остановки ресурса равны нулю. В случае 3 на каждую итерацию приходится 3 из 5 мопов, которые зависят от предыдущей загрузки, поэтому, скорее всего, LM будет заполнен раньше, чем любая другая структура. «Эффективное» количество записей RS, по оценкам, составляет около 51 и 48 на IvB и SnB, соответственно.

(2) Я мог бы сделать здесь безобидное упрощение. См. Возможно ли событие RESOURCE_STALLS.RS произойти, даже если RS не полностью заполнен? .

(3) Может быть полезно создать визуализацию потока uop через конвейер, чтобы увидеть, как все это сочетается. Вы можете использовать простую цепочку загрузки в качестве ссылки. Это легко для случая 1, но трудно для случая 2 из-за воспроизведения.


Метод анализа 2: Использование средства мониторинга производительности задержки нагрузки

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

Альтернативный метод основан на событиях производительности MEM_TRANS_RETIRED.LOAD_LATENCY_*. Эти события являются особыми в том смысле, что они могут быть подсчитаны только на уровне получения p (см .: PERF STAT не учитывает загрузки памяти, но подсчитывает хранилища памяти ).

Например, MEM_TRANS_RETIRED.LOAD_LATENCY_GT_4 подсчитывает количество нагрузок, задержка которых превышает 4 такта ядра "случайно" выбранной выборки всех выполненных нагрузок. Задержка измеряется следующим образом. Цикл, в котором нагрузка отправляется в первый раз, является первым циклом, который считается частью задержки нагрузки. Цикл, в котором записывается результат загрузки, является последним циклом, который считается частью задержки. Следовательно, повторы учитываются. Кроме того, начиная с SnB (как минимум), все нагрузки имеют задержки больше 4 циклов в соответствии с этим определением. Минимальное пороговое значение задержки, которое в настоящее время поддерживается, составляет 3 цикла.

Case 1
Lat Threshold  | Sample Count
 3             | 1426934
 4             | 1505684
 5             | 1439650
 6             | 1032657      << Drop 1
 7             |   47543      << Drop 2
 8             |   57681
 9             |   60803
10             |   76655
11             |     <10      << Drop 3

Case 2
Lat Threshold  | Sample Count
 3             | 1532028
 4             | 1536547
 5             | 1550828
 6             | 1541661
 7             | 1536371
 8             | 1537337
 9             | 1538440
10             | 1531577
11             |     <10      << Drop

Case 3
Lat Threshold  | Sample Count
 3             | 2936547
 4             | 2890162
 5             | 2921158
 6             | 2468704      << Drop 1
 7             | 1242425      << Drop 2
 8             | 1238254
 9             | 1249995
10             | 1240548
11             |     <10      << Drop 3

Очень важно понимать, что эти числа представляют количество нагрузок случайно выбранной выборки всех нагрузок. Например, из общего размера выборки всех нагрузок составляет 10 миллионов, и только 1 миллион из них имеет задержку, превышающую указанный порог, тогда измеренное значение составляет 1 миллион. Однако общее количество выполненных нагрузок может составить 1 миллиард. Поэтому абсолютные значения сами по себе не очень значимы. Что действительно имеет значение, так это картина различных порогов.

В случае 1 наблюдается три значительных снижения количества нагрузок, задержка которых превышает определенный порог. Мы можем сделать вывод, что нагрузки, задержка которых равна или меньше 6 циклов, являются наиболее распространенными, нагрузки, задержка которых равна или меньше 7 циклов, но больше 6 циклов, являются вторыми по распространенности, а большинство других нагрузок имеют задержку между 8-11 циклов.

мы уже знаем, что минимальная задержка составляет 4 цикла. Учитывая эти числа, разумно оценить среднюю задержку загрузки где-то между 4 и 6 циклами, но ближе к 6, чем 4. Мы знаем из метода 1, что средняя задержка нагрузки на самом деле составляет 5.4c. Таким образом, мы можем сделать довольно хорошую оценку, используя эти цифры.

В случае 2 мы можем сделать вывод, что у большинства нагрузок задержка меньше или равна 11 циклам. Средняя задержка нагрузки, вероятно, также намного больше, чем 4, учитывая согласованность измеренного количества нагрузок в широком диапазоне порогов задержки. Таким образом, между 4 и 11, но ближе к 11, чем 4. Мы знаем из метода 1, что средняя задержка нагрузки на самом деле составляет 8,8 с, что близко к любой разумной оценке, основанной на этих числах.

Случай 3 аналогичен случаю 1, и фактически фактическая средняя задержка загрузки, определенная с использованием метода 1, практически одинакова для этих двух случаев.

Выполнение измерений с использованием MEM_TRANS_RETIRED.LOAD_LATENCY_* легко, и такой анализ может быть сделан кем-то, мало знакомым с микроархитектурой.

...