Почему события пропуска хранилища L1 в пользовательском режиме учитываются только при наличии цикла инициализации хранилища? - PullRequest
0 голосов
/ 05 марта 2019

Сводка

Рассмотрим следующий цикл:

loop:
movl   $0x1,(%rax)
add    $0x40,%rax
cmp    %rdx,%rax
jne    loop

, где rax инициализируется адресом буфера, который больше размера кэша L3.Каждая итерация выполняет операцию сохранения до следующей строки кэша.Я ожидаю, что количество запросов RFO, отправленных с L1D на L2, будет более или менее равным количеству строк кэша, к которым осуществляется доступ.Проблема в том, что это, кажется, только тот случай, когда я считаю события в режиме ядра, даже если программа работает в пользовательском режиме, за исключением одного случая, о котором я расскажу ниже.Способ размещения буфера, похоже, не имеет значения (.bss, .data или из кучи).

Подробности

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

Я проверил следующие три случая:

  • Цикл инициализации отсутствует.То есть буфер не доступен до "основного" цикла, показанного выше.Я буду называть это дело NoInit.В этом случае существует только один цикл.
  • Сначала к буферу обращаются, используя одну инструкцию загрузки на строку кэша.Как только все строки будут затронуты, будет выполнен основной цикл.Я буду называть это дело LoadInit.В этом случае есть два цикла:
  • Сначала к буферу обращаются, используя одну инструкцию сохранения на строку кэша.Как только все строки будут затронуты, будет выполнен основной цикл.Я буду называть этот случай StoreInit.В этом случае есть два цикла:

В следующей таблице приведены результаты на процессоре Intel CFL.Эти эксперименты были выполнены на ядре Linux версии 4.4.0.

enter image description here

В следующей таблице приведены результаты для процессора Intel HSW.Обратите внимание, что события L2_RQSTS.PF_HIT, L2_RQSTS.PF_MISS и OFFCORE_REQUESTS.ALL_REQUESTS не задокументированы для HSW.Эти эксперименты были выполнены на ядре Linux версии 4.15.

enter image description here

В первом столбце каждой таблицы содержатся имена событий мониторинга производительности, счетчиками которых являютсяпоказано в других столбцах.В метках столбцов буквы U и K представляют события пользовательского режима и режима ядра соответственно.Для случаев, которые имеют два цикла, числа 1 и 2 используются для ссылки на цикл инициализации и основной цикл, соответственно.Например, LoadInit-1K представляет значения режима ядра для цикла инициализации в случае LoadInit.

Значения, показанные в таблицах, нормализованы по количеству строк кэша.Они также имеют цветовую кодировку следующим образом.Чем темнее зеленый цвет, тем больше значение по отношению ко всем другим ячейкам в той же таблице.Однако последние три строки таблицы CFL и последние две строки таблицы HSW не имеют цветовой кодировки, поскольку некоторые значения в этих строках слишком велики.Эти строки окрашены в темно-серый цвет, что указывает на то, что они не имеют цветовую кодировку, как другие строки.

Я ожидаю, что число событий пользовательского режима L2_RQSTS.ALL_RFO будет равно числу строк кеша(т.е. нормализованное значение 1).Это событие описано в руководстве следующим образом:

Подсчитывает общее количество запросов RFO (чтение для владения) в кэш L2.Запросы RFO L2 включают в себя как пропуски RFO запросов L1D, так и предварительные выборки RF1 L1D.

Это говорит о том, что L2_RQSTS.ALL_RFO может не только подсчитывать запросы RFO запроса от L1D, но также предварительные выборки RFO L1D.Однако я заметил, что на количество событий не влияет то, включены ли предварительные выборщики L1D на обоих процессорах.Но даже если предварительные выборки L1D могут генерировать предварительные выборки RFO, тогда количество событий должно быть по меньшей мере таким же, как и количество строк кэша, к которым был получен доступ.Как видно из обеих таблиц, это только в StoreInit-2U.Это же наблюдение относится ко всем событиям, указанным в таблицах.

Однако количество событий в режиме ядра примерно равно ожидаемому.Это, в отличие от, например, MEM_INST_RETIRED.ALL_STORES (или MEM_UOPS_RETIRED.ALL_STORES на HSW), который работает, как и ожидалось.

Из-за ограниченного количества регистров счетчиков PMU мне пришлось разделить все эксперименты начетыре части.В частности, подсчет режима ядра производится из разных прогонов, нежели подсчет режима пользователя.Неважно, что считается в том же самом.Я думаю, что важно сказать вам об этом, потому что это объясняет, почему некоторые значения в пользовательском режиме немного больше, чем в режиме ядра для тех же событий.

События, показанные темно-серым, кажутся слишком большими.В руководствах по спецификациям процессоров Intel 4-го и 8-го поколения (проблема HSD61 и 111, соответственно) упоминается, что OFFCORE_REQUESTS_OUTSTANDING.DEMAND_RFO может быть превышен.Но эти результаты показывают, что он может быть переоценен во много раз, а не просто через пару событий.

Есть и другие интересные наблюдения, но они не имеют отношения к вопросу, а именно: почему подсчет RFOне так, как ожидалось?

1 Ответ

0 голосов
/ 05 марта 2019

Вы не отметили свою ОС, но давайте предположим, что вы используете Linux.Этот материал будет другим в другой ОС (и, возможно, даже в различных вариантах одной и той же ОС).

При доступе на чтение к не отображенной странице обработчик ошибок страницы ядра отображается в общесистемной общей нулевой странице., с правами только для чтения.

Это объясняет столбцы LoadInit-1U|K: даже если ваша инициализирующая нагрузка превышает виртуальную область 64 МБ, выполняющую нагрузки, только один физический Страница 4K, заполненная нулями, сопоставляется, поэтому вы получаете примерно ноль пропусков кэша после первых 4КБ, которые округляются до нуля после нормализации. 1

При доступе на запись вНеотображенная страница или общая нулевая страница только для чтения, ядро ​​отобразит новую уникальную страницу от имени процесса.Эта новая страница гарантированно обнуляется, поэтому, если в ядре не осталось каких-то известных страниц, которые можно обнулить, это потребует обнуления страницы (фактически memset(new_page, 0, 4096)) до ее отображения.

Это в значительной степениобъясняет остальные столбцы, кроме StoreInit-2U|K.В этих случаях, даже если кажется, что пользовательская программа выполняет все хранилища, ядро ​​в конечном итоге выполняет всю тяжелую работу (за исключением одного хранилища на страницу), поскольку при сбое пользовательского процесса на каждой странице ядро ​​записывает нулик этому, у которого есть побочный эффект переноса всех страниц в кэш L1.Когда обработчик ошибок возвращается, хранилище запуска и все последующие хранилища для этой страницы попадут в кэш L1.

Это все еще не полностью объясняет StoreInit-2.Как поясняется в комментариях, столбец K фактически включает в себя количество пользователей, что объясняет этот столбец (вычитание количества пользователей оставляет его примерно равным нулю для каждого события, как и ожидалось).Оставшаяся путаница заключается в том, почему L2_RQSTS.ALL_RFO - это не 1, а какое-то меньшее значение, например 0,53 или 0,68.Возможно, событие недооценивается, или мы пропускаем какой-то микроархитектурный эффект, например, тип предварительной выборки, который предотвращает RFO (например, если линия загружается в L1 с помощью некоторого типа операции загрузки до сохраненияRFO не произойдет).Вы можете попробовать включить другие L2_RQSTS события, чтобы увидеть, появляются ли отсутствующие события.

Вариации

Это не должно быть так во всех системах.Конечно, другие ОС могут иметь разные стратегии, но даже Linux на x86 может вести себя по-разному в зависимости от различных факторов.

Например, вместо нулевой страницы 4K, вы можете получить огромную нулевую страницу 2 MiB ..Это изменило бы тест, так как 2 MiB не умещается в L1, поэтому тесты LoadInit, вероятно, покажут пропуски в пользовательском пространстве на первом и втором циклах.

В целом, если вы использовали огромные страницы,степень детализации ошибки страницы будет изменена с 4 КиБ до 2 МиБ, что означает, что только небольшая часть обнуленной страницы останется в L1 и L2, так что вы получите пропуски L1 и L2, как вы и ожидали.Если ваше ядро ​​ когда-либо реализует отказ для анонимных отображений (или любого используемого сопоставления), это может иметь аналогичный эффект.

Другая возможность состоит в том, что ядро ​​может обнулять страницы вфон и так готовы к нулю страниц.Это позволило бы удалить K-показатели из тестов, поскольку обнуление не происходит во время ошибки страницы, и, вероятно, добавило бы ожидаемые пропуски к счетчикам пользователя.Я не уверен, что ядро ​​Linux когда-либо делало это или имеет возможность сделать это, но было патчей, плавающих вокруг .Другие ОС, такие как BSD, сделали это.

RFO Prefetchers

О «предварительных сборщиках RFO» - предварительные сборщики RFO на самом деле не являются предварительными сборщиками в обычном смысле, и они не связаны с предварительными выборщиками L1D, которые можно отключить.Насколько я знаю, «предварительная выборка RFO» из L1D просто относится к отправке запроса RFO для хранилищ в буфере хранилища, которые достигают начала буфера хранилища.Очевидно, что когда хранилище попадает в начало буфера, пора отправлять RFO, и вы бы не назвали это предварительной выборкой, но почему бы не отправлять некоторые запросы и на второе хранилище из головы, и так далее?Это предварительные выборки RFO, но они отличаются от обычной предварительной выборки тем, что ядро ​​ знает адрес, который был запрошен: это не предположение.

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


1 Это поведение на самом деле зависит от деталей кэша L1: текущие реализации Intel VIPT допускают несколько виртуальных псевдонимовОдна и та же линия, чтобы все жили счастливо в L1.Текущие реализации AMD Zen используют другую реализацию (микротэги), которая не позволяет L1 логически содержать несколько виртуальных псевдонимов, поэтому я ожидаю, что Zen пропустит L2 в этом случае.

...