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

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

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

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

Edit:

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

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

Edit2:

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

Ответ 1

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

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

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

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

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

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


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

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

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


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

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

Ответ 2

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

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

Вам может быть интересно близость процессора. См. sched_setaffinity (2). Читайте о в реальном времени Linux. См. sched (7). И посмотрите numa (7).

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

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

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