Я нашел комментарий от 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-байтового заполнения. Я что-то не так понял?
@PeterCordes Спасибо! В какой ситуации 128-байтовое заполнение лучше, чем 64-байтовое заполнение?
@QuarticCat некоторые драйверы amd gpu opencl работали лучше с выравниванием 4096 для буферов хоста, но это может быть изменено. Я предполагаю, что это о блоке dma, работающем между vram и ram. Я не знаю, используется ли dma между двумя процессорами на одной материнской плате (вместо их внутренней связи), но когда у меня было 2 графических процессора, более высокое выравнивание работало лучше, чтобы использовать больший процент пропускной способности оперативной памяти только на 1 поток.
Он говорит пространственная предварительная выборка. Ваш эксперимент предназначен для измерения ложного обмена, не связанного с предварительной загрузкой!
«Мой процессор — Ryzen 3700X». Тогда это вообще не Intel, а AMD, и советы по процессорам x86-64 не актуальны.
@KarlKnechtel: Ryzen 3700X является и x86-64; в том числе Intel, AMD, Via и Zhaoxin (возможно, других поставщиков). Это правда, что совет для процессоров Другие x86-64 может быть неприменим. (И программное обеспечение, предназначенное для хорошей работы на процессорах x86-64, в целом должно заботиться о вещах, которые медленны на некоторых, но не на всех процессорах x86-64.) Но обратите внимание, что в вопросе говорится, что они также тестировались на Intel; как показывает мой ответ, этот тест не разработан таким образом, чтобы действительно пострадать из-за пространственной предварительной выборки L2.
о_О Я не привык к такой терминологии. Должно быть, я что-то пропустил по пути.
@ user253751: Пространственная предварительная выборка (соседних строк) очень сильно связана с ложным разделением. Он может причина ложно делиться между объектами в соседних строках, а не только в одной строке. (Но в более ограниченных обстоятельствах, а не на постоянной основе, если строки кеша не продолжают прыгать между ядрами; см. Мой ответ).
@KarlKnechtel: см. Как правильно называть 32-битные и 64-битные версии программ для процессоров x86?. Если вам нужен термин для конкретной реализации Intel x86-64, есть IA-32e или Intel64. x86-64 почти всегда используется для обозначения наименьшего общего знаменателя между Intel и AMD. (например, я мог бы сказать, что x86-64 гарантирует, что загрузка/сохранение в кэшируемой памяти являются атомарными, если они имеют размер степени двойки и содержатся в одном выровненном 8-байтовом qword. Только Intel гарантирует атомарность для любого смещения в пределах 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, но влияние на общее время по-прежнему незначительно.
$ 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% )
$ 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. Связанный:
Буфер хранилища (и переадресация хранилища) выполняет множество приращений для каждой машины с ложным совместным использованием, поэтому все не так плохо, как можно было бы ожидать. (И было бы намного хуже с атомарными 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
, чтобы включить оптимизацию. Я предполагаю, что вы скомпилировали с отключенной оптимизацией, поэтому 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: Интересно, спасибо, что подтвердили мою догадку о том, что истинное разделение между парами потоков будет проблемой для процессоров Intel при использовании смежных строк. И интересно, что это не для AMD. Кстати, можно включать/отключать аппаратные предварительные выборки через MSR, поэтому можно даже убедиться, что отключение пространственной предварительной выборки L2 (или всей предварительной выборки L2) устраняет снижение производительности Intel. (например, Правильно отключить аппаратную предварительную выборку с помощью MSR в Skylake / Действительно ли полезен префетчер L2 HW?)
@ПитерКордес mail-archive.com/[email protected]/msg265387.html. Я думаю, что не совсем ясно, когда пространственная предварительная выборка действительно может вызывать пинг-понг. Прошлые тесты IIRC, которые я запускал, также были очень неоднозначными и сильно различались даже в Intel HW на основе микроархитектуры.
В руководстве по оптимизации Intel описывается пространственная предварительная выборка L2 в процессорах семейства SnB. Да, он пытается завершить выровненные по 128 байт пары строк по 64 байта, когда есть свободная пропускная способность памяти (слоты для отслеживания запросов вне ядра), когда первая строка втягивается.