Торможение кэша процессора

Скажем, у меня есть стандартный x86-процессор defacto с 3 уровнями кэшей, L1 / L2 private и L3, разделяемыми между ядрами. Есть ли способ выделить общую память, данные которой не будут кэшироваться в частных кэшах L1 / L2, а скорее будут кэшироваться только на L3? Я не хочу извлекать данные из памяти (это слишком дорого), но я бы хотел поэкспериментировать с производительностью с использованием и без совместного использования общих данных в частных кэшах.

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

Любое решение (если оно существует) должно выполняться программно, используя C и / или сборку для процессоров на базе процессоров Intel (относительно современные архитектуры Xeon (skylake, widewell)), работающие под управлением операционной системы на базе Linux.

Редактировать:

У меня есть чувствительный к задержке код, который использует форму общей памяти для синхронизации. Данные будут в L3, но при чтении или записи на него будут переходить в L1 / L2 в зависимости от политики включения кэша. Подразумевая проблему, данные должны быть недействительными, добавляя ненужный (я думаю) удар производительности. Я хотел бы посмотреть, можно ли просто хранить данные, либо с помощью какой-либо политики страниц, либо с помощью специальных инструкций только в L3.

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

Edit2:

Я имею дело с параллельными кодами, которые работают на высокопроизводительных системах в течение нескольких месяцев. Системы представляют собой системы с большим числом ядер (например, 40-160 + ядер), которые периодически выполняют синхронизацию, которая должна выполняться в usec.

x86 не имеет возможности делать хранилище, которое обходит или записывает через L1D / L2, но не L3. Есть хранилища NT, которые обходят весь кеш. Все, что заставляет обратную связь с L3 также заставляет полностью записывать обратно в память. (например, clwb ). Они предназначены для использования в энергонезависимых операционных системах или для некогерентного DMA, где важно получить данные, переданные в реальную ОЗУ.

Также нет возможности выполнять нагрузку, которая обходит L1D (кроме USWC-памяти с SSE4.1 movntdqa , но она не является «специальной» для других типов памяти). prefetchNTA может обойти L2, согласно руководству по оптимизации Intel.

Предварительная выборка на ядре, выполняющая чтение, должна быть полезна для запуска обратной записи из другого ядра в L3 и передачи в ваш собственный L1D. Но это полезно, только если у вас есть адрес, прежде чем вы захотите выполнить загрузку. (Десятки циклов, чтобы это было полезно.)

Процессоры Intel используют общий ansible кэш L3 в качестве блокировки для согласованности кеша на кристалле. 2-socket должен отследить другой сокет, но Xeons, которые поддерживают более 2P, имеют фильтры snoop для отслеживания строк кэша, которые перемещаются.

Когда вы читаете строку, недавно написанную другим kernelм, она всегда недействительна в вашем L1D. L3 имеет значение tag-inclusive, и его tags содержат дополнительную информацию для отслеживания того, какое kernel ​​имеет линию. (Это верно, даже если строка находится в состоянии M в L1D где-то, что требует, чтобы она была недопустимой в L3, в соответствии с нормальным MESI .) Таким образом, после того, как ваши кеш-промах проверяет tags L3, он вызывает запрос к L1 у которого есть строка, чтобы записать его обратно в кеш-память L3 (и, возможно, отправить его прямо в kernel, чем хочет).

Skylake-X (Skylake-AVX512) не имеет инклюзивного L3 (у него более крупный частный L2 и меньший L3), но он по-прежнему имеет структуру, содержащую tags, чтобы отслеживать, какое kernel ​​имеет линию. Он также использует сетку вместо кольца, а латентность L3, по-видимому, значительно хуже, чем Бродвелл.


Возможно, полезно: сопоставьте критическую для латентности часть области разделяемой памяти с политикой кэширования для записи. IDK, если этот патч когда-либо попадал в основное kernel ​​Linux, но см. Этот патч от HP: поддержка Write-Through mapping на x86 . (Обычная политика – WB.)

Также были связаны: основные характеристики памяти и кэша Intel Sandy Bridge и AMD Bulldozer , подробный анализ латентности и пропускной способности на 2-сокетном SnB для строк кэша в разных начальных состояниях.

Дополнительные сведения о пропускной способности памяти на процессорах Intel см. В разделе Расширенное REP MOVSB ​​для memcpy , особенно в разделе «Платформы ограниченной привязки ». (Наличие только 10 LFBs ограничивает одноядерную полосу пропускания).


Связанные: Каковы задержки и пропускные затраты между распределением памяти между гипер-братьями и братьями и сестрами? имеет некоторые экспериментальные результаты для того, чтобы один stream спама записывал в место, в то время как другой stream читает его.

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

Вы не найдете хороших способов отключить использование L1 или L2 для процессоров Intel: действительно, за пределами нескольких конкретных сценариев, таких как области памяти UC, которые описаны в ответе Питера (что убьет вашу производительность, поскольку они не используют L3) , в частности, L1 в основном участвует в чтении и записи.

Однако вы можете использовать довольно корректное поведение кеша L1 и L2, чтобы принудительно вытеснить данные, которые вы хотите жить только в L3. На последних архитектурах Intel L1 и L2 ведут себя как псевдо-LRU «стандартные ассоциативные» кэши. Под «стандартным ассоциативным» я подразумеваю структуру кэша, о которой вы читали в википедии или на своем аппаратном курсе, где кеш делится на 2 ^ N множества, которые имеют M записи (для ассоциативного кэша M -way) и N последовательных битов от адреса используются для поиска набора.

Это означает, что вы можете точно предсказать, какие строки кэша окажутся в одном наборе. Например, Skylake имеет 8-полосный 32K L1D и 4-way 256K L2. Это означает, что кэш-линии на расстоянии 64 КБ будут попадать в один и тот же набор на L1 и L2. Обычно использование сильно используемых значений попадает в одну и ту же строку кэша – проблема (конфликт в кеш-наборе может сделать ваш кеш намного меньше, чем есть на самом деле), но здесь вы можете использовать его в своих интересах!

Если вы хотите вырезать линию из L1 и L2, просто прочитайте или напишите 8 или более значений другим строкам, расположенным на расстоянии 64K от целевой линии. В зависимости от структуры вашего теста (или основного приложения) вам может даже не понадобиться фиктивная запись: в вашем внутреннем цикле вы можете просто использовать использование 16 значений, все разнесенные на 64K, и не возвращаться к первому значению, пока вы не посетили другой 15. Таким образом, каждая линия будет «естественным образом» выseleniumа, прежде чем вы ее используете.

Обратите внимание, что записи манекенов не обязательно должны быть одинаковыми на каждом ядре: каждое kernel ​​может записывать на «частные» фиктивные строки, чтобы вы не добавляли соперничество для фиктивных записей.

Некоторые осложнения:

  • Адреса, которые мы обсуждаем здесь (когда мы говорим такие вещи, как «64K от целевого адреса»), являются физическими адресами. Если вы используете 4K-страницы, вы можете выселить из L1, записав в смещениях 4K, но чтобы заставить его работать для L2, вам нужны 64-кратные физические смещения, но вы не можете получить это надежно, так как каждый раз, когда вы пересекаете страницу 4K границы, которую вы пишете на какую-либо произвольную физическую страницу. Вы можете решить эту проблему, убедившись, что вы используете 2MB огромные страницы для задействованных строк кеша.
  • Я сказал, что «8 или более » строк кеша нужно читать / писать. Это связано с тем, что кэши скорее всего используют какой-то псевдо-LRU, а не точный LRU. Вам придется протестировать: вы можете обнаружить, что псевдо-LRU работает точно так же, как точный LRU для используемого вами шаблона, или вы можете обнаружить, что вам нужно более 8 записей, чтобы выселить надежно.

Некоторые другие примечания:

  • Вы можете использовать счетчики производительности, выставленные perf чтобы определить, как часто вы на самом деле нажимаете L1 против L2 против L3, чтобы убедиться, что ваш трюк работает.
  • L3 обычно не является «стандартным ассоциативным кешем»: скорее, набор рассматривается путем hashирования большего количества битов адреса, чем типичный кеш. Хеширование означает, что вы не сможете использовать только несколько строк в L3: ваши целевые и фиктивные строки должны быть хорошо распределены вокруг L3. Если вы обнаружите, что используете незащищенный L3, он все равно должен работать (потому что L3 больше, вы все равно будете распространяться среди наборов кешей), но вам нужно быть более осторожным в отношении возможных выseleniumий из L3.

Недавно Intel анонсировала новую инструкцию, которая, похоже, имеет отношение к этому вопросу. Инструкция называется CLDEMOTE. Он перемещает данные из кэшей более высокого уровня в кеш более низкого уровня. (Вероятно, от L1 или L2 до L3, хотя спецификация неточна в деталях.) «Это может ускорить последующие обращения к линии другими ядрами …»

https://software.intel.com/sites/default/files/managed/c5/15/architecture-instruction-set-extensions-programming-reference.pdf

Я считаю, вы не должны (и, вероятно, не можете) заботиться, и надеемся, что разделяемая память находится в L3. BTW, код пользователя C-кода работает в виртуальном адресном пространстве, а ваши другие ядра могут (и часто) запускать какой-то другой несвязанный процесс .

Аппаратное обеспечение и MMU (который настроен kernelм) гарантируют, что L3 будет правильно распространен.

но я бы хотел поэкспериментировать с производительностью с использованием и без приведения общих данных в частные кеши.

Насколько я понимаю (совсем плохо) недавнее оборудование Intel, это невозможно (по крайней мере, не в пользовательской зоне).

Возможно, вы могли бы рассмотреть инструкцию PREFETCH и встроенный GCC __builtin_prefetch (что делает противоположное тому, что вы хотите, оно приводит данные к более близким кэшам). Смотрите, что и что .

BTW, kernel ​​делает превентивное планирование, поэтому переключатели контекста могут произойти в любой момент (часто несколько сотен раз в секунду). Когда (во время переключения контекста) другой процесс запланирован на одном и том же ядре, необходимо переконфигурировать MMU (поскольку каждый процесс имеет свое виртуальное адресное пространство , а кэши снова «холодно»).

Возможно, вас интересует близость процессора . См. Sched_setaffinity (2) . Читайте о реальном Linux . См. Расписание (7) . И см. Numa (7) .

Я совсем не уверен, что производительность, поразившая вас, боится, что это примечательно (и я считаю, что этого нельзя избежать в пользовательском пространстве).

Возможно, вы можете подумать о переносе своего секретного кода в пространстве ядра (например, с привилегией CPL0), но, возможно, это требует месяцев работы и, вероятно, не стоит усилий. Я даже не попробую.

Рассматривали ли вы другие совершенно разные подходы (например, переписываете его в OpenCL для своего GPGPU) на свой чувствительный к задержке код?