Я думаю, что ответ @ BeeOnRope полностью отвечает на мой вопрос.Я хотел бы добавить некоторые дополнительные детали, основанные на ответе @ BeeOnRope и комментариях под ним.В частности, я покажу, как определить, происходит ли событие производительности фиксированное число раз за итерацию для всех шагов загрузки или нет.
Легко увидеть, посмотрев код, который занимает 3 мопавыполнить одну итерацию.Первые несколько загрузок могут отсутствовать в кэше L1, но затем все последующие загрузки будут попадать в кэш, поскольку все виртуальные страницы отображаются на одну и ту же физическую страницу, а L1 в процессорах Intel физически помечены и проиндексированы.Итак, 3 мопса.Теперь рассмотрим событие производительности UOPS_RETIRED.ALL
, которое происходит, когда моп удаляется.Мы ожидаем увидеть около 3 * number of iterations
таких событий.Аппаратные прерывания и сбои страниц, возникающие во время выполнения, требуют помощи микрокода для обработки, что, вероятно, нарушит производительность.Следовательно, для конкретного измерения события X производительности источником каждого подсчитанного события может быть:
- Инструкции профилируемого кода.Давайте назовем это X 1 .
- Упс, используемый для вызова сбоя страницы, который произошел из-за доступа к памяти, предпринятого профилируемым кодом.Давайте назовем это X 2 .
- Uops, используемый для вызова обработчика прерываний из-за асинхронного аппаратного прерывания или для вызова программной исключительной ситуации.Давайте назовем это X 3 .
Следовательно, X = X 1 + X 2 + X 3 .
Поскольку код прост, мы смогли определить с помощью статического анализа, что X 1 = 3. Но мы ничего не знаем о X 2 и X 3 , который не может быть постоянным на одну итерацию.Мы можем измерить X, используя UOPS_RETIRED.ALL
.К счастью, для нашего кода количество сбоев страниц соответствует регулярному шаблону: ровно по одному на каждую страницу (что можно проверить с помощью perf
).Разумно предположить, что для устранения каждой ошибки на странице требуется одинаковый объем работы, поэтому каждый раз он будет оказывать одинаковое влияние на X.Обратите внимание, что это отличается от количества сбоев страниц за итерацию, которое отличается для разных шагов загрузки.Число мопов, удаленных как прямой результат выполнения цикла для каждой страницы, является постоянным.Наш код не вызывает никаких программных исключений, поэтому нам не нужно о них беспокоиться.Как насчет аппаратных прерываний?Что ж, в Linux, пока мы запускаем код на ядре, которое не предназначено для обработки прерываний мыши / клавиатуры, единственное прерывание, которое действительно имеет значение, это локальный таймер APIC.К счастью, это прерывание также происходит регулярно.Пока количество времени, затрачиваемое на страницу, одинаково, влияние прерывания таймера на X будет постоянным для каждой страницы.
Мы можем упростить предыдущее уравнение до:
X =X 1 + X 4 .
Таким образом, для всех нагрузок
(X на страницу) - (X 1 на страницу) = (X 4 на страницу) = константа.
Теперь я расскажу, почему это полезно, и приведу примеры использования различных событий производительности.Нам понадобятся следующие обозначения:
ec = total number of performance events (measured)
np = total number of virtual memory mappings used = minor page faults + major page faults (measured)
exp = expected number of performance events per iteration *on average* (unknown)
iter = total number of iterations. (statically known)
Обратите внимание, что в целом мы не знаем или не уверены в интересующем нас событии производительности, поэтому нам необходимоизмерить это.Случай с отставными мопами был легким.Но в целом это то, что нам нужно выяснить или проверить экспериментально.По сути, exp
- это число событий производительности ec
, но исключая их при возникновении ошибок и прерываний страницы.
На основе аргументов и предположений, изложенных выше, мы можем вывести следующее уравнение:
C = (ec/np) - (exp*iter/np) = (ec - exp*iter)/np
Здесь есть два неизвестных: константа C
и интересующее нас значение exp
.Итак, нам нужно два уравнения, чтобы можно было вычислить неизвестные.Поскольку это уравнение выполняется для всех шагов, мы можем использовать измерения для двух разных шагов:
C = (ec 1 - exp * iter) / np 1
C = (ec 2 - exp * iter) / np 2
Мы можем найти exp
:
(ec 1 - exp * iter) / np 1 = (ec 2 - exp * iter) / np 2
ec 1 * np 2 - exp * iter * np 2 = ec 2 * np 1 - exp * iter * np 1
ec 1 * np 2 - ec 2 * np 1 = exp * iter * np 2 - exp * iter * np 1
ec 1 * np 2 - ec 2 * np 1 = exp * iter * (np 2 - np 1 )
Таким образом,
exp = (ec 1 * np 2 - ec 2 * np 1 ) / (iter * (np 2 - np 1 ))
Давайте применим это уравнение кUOPS_RETIRED.ALL
.
шаг 1 = 32
iter = 10 миллионов
np 1 = 10 миллионов * 32/4096 = 78125
ес 1 = 51410801
шаг 2 = 64
iter = 10 миллионов
np 2 = 10 миллионов * 64/4096 = 156250
ec 2 = 72883662
exp = (51410801* 156250 - 72883662 * 78125) / (10 м * (156250 - 78125))
= 2,99
Отлично!Очень близко к ожидаемым 3 пенсиям в отставке за итерацию.
C = (51410801 - 2,99 * 10 м) / 78125 = 275,3
Я рассчитал C
для всех шагов.Это не совсем константа, но это 275 + -1 для всех шагов.
exp
для других событий производительности можно получить аналогично:
MEM_LOAD_UOPS_RETIRED.L1_MISS
: exp
= 0
MEM_LOAD_UOPS_RETIRED.L1_HIT
: exp
= 1
MEM_UOPS_RETIRED.ALL_LOADS
: exp
= 1
UOPS_RETIRED.RETIRE_SLOTS
: exp
= 3
Так работает ли это для всех событий производительности?Что ж, давайте попробуем что-то менее очевидное.Рассмотрим, например, RESOURCE_STALLS.ANY
, который измеряет циклы задержки распределителя по любой причине.Трудно сказать, сколько должно быть exp
, просто взглянув на код.Обратите внимание, что для нашего кода RESOURCE_STALLS.ROB
и RESOURCE_STALLS.RS
равны нулю.Только RESOURCE_STALLS.ANY
здесь имеет значение.Вооружившись уравнением для exp
и экспериментальными результатами для разных шагов, мы можем рассчитать exp
.
шаг 1 = 32
iter = 10 миллионов
np 1 = 10 миллионов * 32/4096 = 78125
ec 1 = 9207261
шага 2 = 64
iter = 10миллион
np 2 = 10 миллионов * 64/4096 = 156250
ec 2 = 16111308
exp = (9207261 * 156250 - 16111308 * 78125) / (10 м * (156250 - 78125))
= 0,23
C = (9207261 - 0,23 * 10 м) / 78125 = 88,4
Я рассчитал C
для всехшагает.Ну, это не выглядит постоянным.Возможно, мы должны использовать разные шаги?Без вреда при попытках.
шаг 1 = 32
iter 1 = 10 миллионов
np 1 = 10 миллионов *32/4096 = 78125
ec 1 = 9207261
шага 2 = 4096
iter 2 = 1 миллион
np 2 = 1 миллион * 4096/4096 = 1 м
ec 2 = 122563371
exp = (9207261 * 1 м - 102563371 * 78125) / (1м * 1м - 10м * 78125))
= 0,01
C = (9207261 - 0,23 * 10м) / 78125 = 88,4
(Обратите внимание, что на этот раз я использовал другое количество итерацийпросто чтобы показать, что вы можете это сделать.)
Мы получили другое значение для exp
.Я рассчитал C
для всех шагов, и он все еще не выглядит постоянным, как показано на следующем графике.Он значительно варьируется для более мелких шагов, а затем немного после 2048 года. Это означает, что одно или несколько предположений о наличии фиксированного количества циклов задержки распределителя на страницу не являются действительными в такой степени.Другими словами, стандартное отклонение циклов задержки распределителя для разных шагов является значительным.
![enter image description here](https://i.stack.imgur.com/EX2BO.png)
Для события производительности UOPS_RETIRED.STALL_CYCLES
, exp
= -0,32 и стандартное отклонение также значимо.Это означает, что одно или несколько предположений о наличии фиксированного количества циклов отставания на одной странице недействительны.
![enter image description here](https://i.stack.imgur.com/vLHQK.png)
Я разработал простой способ исправить измеренное количество вышедших на пенсию инструкций. Каждый сбой вызванной страницы добавляет ровно одно дополнительное событие к счетчику удаленных команд. Например, предположим, что сбой страницы происходит регулярно после некоторого фиксированного числа итераций, например 2. То есть, каждые две итерацииошибка вызвана.Это происходит для кода в вопросе, когда шаг равен 2048. Поскольку мы ожидаем, что 4 инструкции будут отменены за одну итерацию, общее число ожидаемых удаленных инструкций до появления ошибки страницы будет равно 4 * 2 = 8. Поскольку ошибка страницы добавляет однудополнительное событие для счетчика удаленных команд, оно будет измеряться как 9 для двух итераций вместо 8. То есть 4,5 за итерацию.Когда я на самом деле измеряю количество выбывших инструкций для случая с шагом 2048, оно очень близко к 4,5.Во всех случаях, когда я применяю этот метод для статического прогнозирования значения измеренной удаленной инструкции за одну итерацию, ошибка всегда составляет менее 1%.Это очень точно, несмотря на аппаратные прерывания.Я думаю, что пока общее время выполнения составляет менее 5 миллиардов циклов ядра, аппаратные прерывания не будут оказывать существенного влияния на счетчик удаленных команд.(Каждый из моих экспериментов занимал не более 5 миллиардов циклов, вот почему.) Но, как объяснялось выше, всегда следует обращать внимание на количество возникших неисправностей.
Как я уже говорил выше, существует многосчетчики производительности, которые можно исправить, рассчитав значения для каждой страницы.С другой стороны, счетчик удаленных команд может быть исправлен с учетом количества итераций, чтобы получить ошибку страницы.RESOURCE_STALLS.ANY
и UOPS_RETIRED.STALL_CYCLES
, возможно, могут быть исправлены аналогично счетчику удаленных команд, но я не исследовал эти два.