Должен ли размер заполнения кэша x86-64 составлять 128 байт?

Я нашел комментарий от crossbeam.

Starting from Intel's Sandy Bridge, spatial prefetcher is now pulling pairs of 64-byte cache lines at a time, so we have to align to 128 bytes rather than 64.

Sources:

Я не нашел строки в руководстве Intel, говорящей об этом. Но до последнего коммита folly по-прежнему использует 128-байтовый отступ, что делает его убедительным. Поэтому я начал писать код, чтобы посмотреть, смогу ли я наблюдать такое поведение. Вот мой код.

#include <thread>

int counter[1024]{};

void update(int idx) {
    for (int j = 0; j < 100000000; j++) ++counter[idx];
}

int main() {
    std::thread t1(update, 0);
    std::thread t2(update, 1);
    std::thread t3(update, 2);
    std::thread t4(update, 3);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

Проводник компилятора

Мой процессор Ryzen 3700X. Когда индексы 0, 1, 2, 3, для завершения требуется ~ 1,2 с. Когда индексы 0, 16, 32, 48, для завершения требуется ~ 200 мс. Когда индексы 0, 32, 64, 96, для завершения требуется ~ 200 мс, что точно так же, как и раньше. Я также проверил их на машине Intel, и это дало мне аналогичный результат.

С этой микроскамейки я не вижу причин использовать 128-байтовое заполнение вместо 64-байтового заполнения. Я что-то не так понял?

В руководстве по оптимизации Intel описывается пространственная предварительная выборка L2 в процессорах семейства SnB. Да, он пытается завершить выровненные по 128 байт пары строк по 64 байта, когда есть свободная пропускная способность памяти (слоты для отслеживания запросов вне ядра), когда первая строка втягивается.

Peter Cordes 05.05.2022 13:45

@PeterCordes Спасибо! В какой ситуации 128-байтовое заполнение лучше, чем 64-байтовое заполнение?

QuarticCat 05.05.2022 13:50

@QuarticCat некоторые драйверы amd gpu opencl работали лучше с выравниванием 4096 для буферов хоста, но это может быть изменено. Я предполагаю, что это о блоке dma, работающем между vram и ram. Я не знаю, используется ли dma между двумя процессорами на одной материнской плате (вместо их внутренней связи), но когда у меня было 2 графических процессора, более высокое выравнивание работало лучше, чтобы использовать больший процент пропускной способности оперативной памяти только на 1 поток.

huseyin tugrul buyukisik 05.05.2022 13:53

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

user253751 05.05.2022 14:33

«Мой процессор — Ryzen 3700X». Тогда это вообще не Intel, а AMD, и советы по процессорам x86-64 не актуальны.

Karl Knechtel 05.05.2022 14:34

@KarlKnechtel: Ryzen 3700X является и x86-64; в том числе Intel, AMD, Via и Zhaoxin (возможно, других поставщиков). Это правда, что совет для процессоров Другие x86-64 может быть неприменим. (И программное обеспечение, предназначенное для хорошей работы на процессорах x86-64, в целом должно заботиться о вещах, которые медленны на некоторых, но не на всех процессорах x86-64.) Но обратите внимание, что в вопросе говорится, что они также тестировались на Intel; как показывает мой ответ, этот тест не разработан таким образом, чтобы действительно пострадать из-за пространственной предварительной выборки L2.

Peter Cordes 05.05.2022 14:37

о_О Я не привык к такой терминологии. Должно быть, я что-то пропустил по пути.

Karl Knechtel 05.05.2022 14:38

@ user253751: Пространственная предварительная выборка (соседних строк) очень сильно связана с ложным разделением. Он может причина ложно делиться между объектами в соседних строках, а не только в одной строке. (Но в более ограниченных обстоятельствах, а не на постоянной основе, если строки кеша не продолжают прыгать между ядрами; см. Мой ответ).

Peter Cordes 05.05.2022 14:41

@KarlKnechtel: см. Как правильно называть 32-битные и 64-битные версии программ для процессоров x86?. Если вам нужен термин для конкретной реализации Intel x86-64, есть IA-32e или Intel64. x86-64 почти всегда используется для обозначения наименьшего общего знаменателя между Intel и AMD. (например, я мог бы сказать, что x86-64 гарантирует, что загрузка/сохранение в кэшируемой памяти являются атомарными, если они имеют размер степени двойки и содержатся в одном выровненном 8-байтовом qword. Только Intel гарантирует атомарность для любого смещения в пределах 1 строки кэша. .)

Peter Cordes 05.05.2022 14:45
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
12
9
355
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В руководстве по оптимизации Intel описывается пространственная предварительная выборка L2 в процессорах семейства SnB. Да, он пытается завершить выровненные по 128 байт пары строк по 64 байта, когда есть свободная пропускная способность памяти (слоты для отслеживания запросов вне ядра), когда первая строка втягивается.

Ваш микротест не показывает существенной разницы во времени между разделением 64 и 128 байт. Без какого-либо ложного совместного использования действительный (в пределах той же 64-байтовой строки) после некоторого начального хаоса он быстро достигает состояния, в котором каждое ядро ​​​​имеет исключительные права на изменяемую им строку кэша. Это означает, что L1d больше не промахнется, а значит, не будет запросов к L2, которые вызовут запуск пространственной предварительной выборки L2.

В отличии от например две пары потоков, борющихся за отдельные atomic<int> переменные в соседних (или нет) строках кэша. Или ложного обмена с ними. Тогда пространственная предварительная выборка L2 может объединить конкуренцию, так что все 4 потока соревнуются друг с другом, а не 2 независимые пары. По сути, в любом случае, когда строки кэша на самом деле скачут туда-сюда между ядрами, пространственная предварительная выборка L2 может ухудшить ситуацию, если вы не будете осторожны.

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

Понимание std::hardware_destructive_interference_size и std::hardware_constructive_interference_size включает ответ с более длинным тестом; Я не смотрел на него в последнее время, но я думаю, что он должен демонстрировать деструктивную интерференцию на 64 байтах, а не на 128. Ответ там, к сожалению, не упоминает пространственную предварительную выборку L2 как один из эффектов, которые могут вызвать деструктивную интерференцию некоторый (хотя и не столько же, сколько 128-байтовый размер строки на внешнем уровне кеша, особенно если это инклюзивный кеш).


Счетчики производительности показывают разницу даже с вашим тестом

На является больше начального хаоса, который мы можем измерить с помощью счетчиков производительности для вашего теста. На моем i7-6700k (четырехъядерный Skylake с Hyperthreading; 4c8t, работающий под управлением Linux 5.16) я улучшил исходный код, чтобы я мог компилировать с оптимизацией, не нарушая доступ к памяти, и с помощью макроса CPP, чтобы я мог установить шаг (в байтах). ) из командной строки компилятора. Обратите внимание на примерно 500 ядерных ударов по неправильному порядку памяти (machine_clears.memory_ordering), когда мы используем соседние строки. Фактическое число весьма изменчиво, от 200 до 850, но влияние на общее время по-прежнему незначительно.

Смежные линии, 500 +- 300 машинных очисток

$ g++ -DSIZE=64 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out 

 Performance counter stats for './a.out' (25 runs):

            560.22 msec task-clock                #    3.958 CPUs utilized            ( +-  0.12% )
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
               126      page-faults               #  224.752 /sec                     ( +-  0.35% )
     2,180,391,747      cycles                    #    3.889 GHz                      ( +-  0.12% )
     2,003,039,378      instructions              #    0.92  insn per cycle           ( +-  0.00% )
     1,604,118,661      uops_issued.any           #    2.861 G/sec                    ( +-  0.00% )
     2,003,739,959      uops_executed.thread      #    3.574 G/sec                    ( +-  0.00% )
               494      machine_clears.memory_ordering #  881.172 /sec                     ( +-  9.00% )

          0.141534 +- 0.000342 seconds time elapsed  ( +-  0.24% )

по сравнению со 128-байтовым разделением только очень немногие машины очищают

$ g++ -DSIZE=128 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out 

 Performance counter stats for './a.out' (25 runs):

            560.01 msec task-clock                #    3.957 CPUs utilized            ( +-  0.13% )
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
               124      page-faults               #  221.203 /sec                     ( +-  0.16% )
     2,180,048,243      cycles                    #    3.889 GHz                      ( +-  0.13% )
     2,003,038,553      instructions              #    0.92  insn per cycle           ( +-  0.00% )
     1,604,084,990      uops_issued.any           #    2.862 G/sec                    ( +-  0.00% )
     2,003,707,895      uops_executed.thread      #    3.574 G/sec                    ( +-  0.00% )
                22      machine_clears.memory_ordering #   39.246 /sec                     ( +-  9.68% )

          0.141506 +- 0.000342 seconds time elapsed  ( +-  0.24% )

Предположительно, есть некоторая зависимость от того, как Linux распределяет потоки по логическим ядрам на этой машине 4c8t. Связанный:

по сравнению с фактическим ложным обменом в одной строке: 10 миллионов машинных очисток

Буфер хранилища (и переадресация хранилища) выполняет множество приращений для каждой машины с ложным совместным использованием, поэтому все не так плохо, как можно было бы ожидать. (И было бы намного хуже с атомарными RMW, такими как std::atomic<int>fetch_add, поскольку там каждому отдельному приращению требуется прямой доступ к кешу L1d во время его выполнения.) Почему ложное совместное использование все еще влияет на не атомарные, но гораздо меньше, чем на атомарные?

$ g++ -DSIZE=4 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out 

 Performance counter stats for './a.out' (25 runs):

            809.98 msec task-clock                #    3.835 CPUs utilized            ( +-  0.42% )
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
               122      page-faults               #  152.953 /sec                     ( +-  0.22% )
     3,152,973,230      cycles                    #    3.953 GHz                      ( +-  0.42% )
     2,003,038,681      instructions              #    0.65  insn per cycle           ( +-  0.00% )
     2,868,628,070      uops_issued.any           #    3.596 G/sec                    ( +-  0.41% )
     2,934,059,729      uops_executed.thread      #    3.678 G/sec                    ( +-  0.30% )
        10,810,169      machine_clears.memory_ordering #   13.553 M/sec                    ( +-  0.90% )

           0.21123 +- 0.00124 seconds time elapsed  ( +-  0.59% )

Улучшенный бенчмарк — выравнивание массива и volatile для оптимизации.

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

И я использовал alignas(128) counter[], чтобы мы были уверены, что начало массива находится в двух парах 128-байтовых строк, а не в трех.

#include <thread>

alignas(128) volatile int counter[1024]{};

void update(int idx) {
    for (int j = 0; j < 100000000; j++) ++counter[idx];
}

static const int stride = SIZE/sizeof(counter[0]);
int main() {
    std::thread t1(update, 0*stride);
    std::thread t2(update, 1*stride);
    std::thread t3(update, 2*stride);
    std::thread t4(update, 3*stride);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

Следуя вашим инструкциям, я создал еще один ориентир. И это показывает значительную разницу между 64 байтами и 128 байтами на Intel, но не на AMD. Я заметил, что на обеих платформах результат иногда сильно различается. Я думаю, что это связано с планировщиком.

QuarticCat 06.05.2022 08:22

@QuarticCat: Интересно, спасибо, что подтвердили мою догадку о том, что истинное разделение между парами потоков будет проблемой для процессоров Intel при использовании смежных строк. И интересно, что это не для AMD. Кстати, можно включать/отключать аппаратные предварительные выборки через MSR, поэтому можно даже убедиться, что отключение пространственной предварительной выборки L2 (или всей предварительной выборки L2) устраняет снижение производительности Intel. (например, Правильно отключить аппаратную предварительную выборку с помощью MSR в Skylake / Действительно ли полезен префетчер L2 HW?)

Peter Cordes 06.05.2022 08:31

@ПитерКордес mail-archive.com/[email protected]/msg265387.html. Я думаю, что не совсем ясно, когда пространственная предварительная выборка действительно может вызывать пинг-понг. Прошлые тесты IIRC, которые я запускал, также были очень неоднозначными и сильно различались даже в Intel HW на основе микроархитектуры.

Noah 10.05.2022 02:46

Другие вопросы по теме