Интересная идея, да, это, вероятно, должно привести к тому, что строка кеша, удерживающая вашу структуру, в состоянии в кеше L3, где ядро # 2 может получить попадание L3 напрямую вместо того, чтобы ждать запроса чтения MESI в то время как линия все еще находится в состоянии M в L1d ядра # 2.
Или, если ProcessD работает на другом логическом ядре того же физического ядра, что и ProcessB, данные будут выбраны в правильный L1d . Если он проводит большую часть своего времени в спящем режиме (и нечасто просыпается), ProcessB по-прежнему обычно будет иметь весь ЦП, работающий в однопоточном режиме без разделения ROB и буфера хранения.
Вместо того, чтобы поток фиктивного доступа вращался на usleep(10)
, , вы могли бы ожидать, что он будет ожидать переменную условия или семафор, который ProcessC выдает после записи glbXYZ .
Со счетным семафором (как семафоры POSIX C sem_wait
/ sem_post
) поток, который записывает glbXYZ
, может увеличивать семафор, вызывая ОС для пробуждения ProcessD, который заблокирован в sem_down
. Если по какой-то причине ProcessD пропустит свою очередь, чтобы проснуться, он выполнит 2 итерации, прежде чем снова заблокировать, но это нормально. (Хм, так что на самом деле нам не нужен подсчитывающий семафор, но я думаю, что нам нужен сон / пробуждение с помощью ОС, и это простой способ получить его, если только мы не хотим избежать издержек, связанных с системным вызовом в processC после написание структуры.) Или системный вызов raise()
в ProcessC может отправить сигнал для запуска пробуждения ProcessD.
При смягчении Specter + Meltdown любой системный вызов, даже такой эффективный, как Linux futex
, довольно дорог для потока, его создающего. Однако эта стоимость не является частью критического пути, который вы пытаетесь сократить, и все же она намного меньше, чем 10-дневный сон, о котором вы думали между извлечениями.
void ProcessD(void) {
while(1){
sem_wait(something); // allows one iteration to run per sem_post
__builtin_prefetch (&glbXYZ, 0, 1); // PREFETCHT2 into L2 and L3 cache
}
}
(Согласно руководству по оптимизации Intel, раздел 7.3.2 , PREFETCHT2 на текущих процессорах идентичен PREFETCHT1 и извлекается в кэш L2 (и L3 по пути. Я не проверял AMD).
На какой уровень кэша извлекается PREFETCHT2? ).
Я не проверял, что PREFETCHT2 действительно будет полезен здесь на процессорах Intel или AMD. Возможно, вы захотите использовать фиктивный volatile
доступ типа *(volatile char*)&glbXYZ;
или *(volatile int*)&glbXYZ.field1
. Особенно, если у вас ProcessD работает на том же физическом ядре, что и ProcessB.
Если prefetchT2
работает, вы можете сделать это в потоке, который пишет bDOIT
(ProcessA), чтобы он мог инициировать миграцию строки в L3 непосредственно перед тем, как ProcessB понадобится.
Если вы обнаружите, что строка высвобождается перед использованием, возможно, вы делаете хотите, чтобы поток извлекался при извлечении этой строки кэша.
На будущих процессорах Intel есть инструкция cldemote
(_cldemote(const void*)
) , которую вы можете использовать после записи, чтобы инициировать миграцию грязной строки кэша в L3. Он работает как NOP на процессорах, которые его не поддерживают, но пока он рассчитан только на Tremont (Atom) . (Наряду с umonitor
/ umwait
для пробуждения, когда другое ядро пишет в контролируемом диапазоне из пользовательского пространства, что, вероятно, также было бы очень полезно для межъядерного содержимого с низкой задержкой.)
Поскольку ProcessA не пишет структуру, вы, вероятно, должны убедиться, что bDOIT
находится в другой строке кэша, чем структура. Вы можете поместить alignas(64)
в первый член XYZ
, чтобы структура начиналась в начале строки кэша. alignas(64) atomic<int> bDOIT;
удостоверится, что это также было в начале строки, чтобы они не могли совместно использовать строку кэша. Или сделайте это alignas(64) atomic<bool>
или atomic_flag
.
Также см. Понимание std :: hardware_destructive_interference_size и std :: hardware_constructive_interference_size 1 : обычно 128 - это то, что вы хотите избежать ложного совместного использования из-за предварительных выборок соседней линии, но на самом деле это не так плохо, если ProcessB запускает предварительный выборщик смежной линии L2 на ядре # 2, чтобы умозрительно выдвинуть glbXYZ
в свой кэш L2, когда он вращается на bDOIT
. Так что вы можете сгруппировать их в 128-байтовую выровненную структуру, если вы используете процессор Intel.
И / или вы можете даже использовать программную предварительную выборку, если bDOIT
имеет значение false, в processB. Предварительная выборка не будет блокировать ожидание данных, но если запрос на чтение поступает в середине ProcessC пишет glbXYZ
, тогда это займет больше времени. Так, может быть, только предварительная выборка SW каждый 16-й или 64-й раз bDOIT
является ложной?
И не забывайте использовать _mm_pause()
в своем цикле вращения, чтобы избежать нюка конвейера неправильной спекуляции порядка памяти, когда ветвь, на которую вы вращаетесь, идет другим путем. (Обычно это ветвь с выходом из цикла в цикле ожидания с вращением, но это не имеет значения. Ваша логика ветвления эквивалентна внешнему бесконечному циклу, содержащему цикл ожидания с вращением, а затем некоторую работу, даже если вы ее не так написали .)
Или, возможно, использовать lock cmpxchg
вместо чистой загрузки, чтобы прочитать старое значение. Полные барьеры уже блокируют спекулятивные нагрузки после барьера, поэтому не допускайте ошибочных спекуляций. (Вы можете сделать это в C11 с atomic_compare_exchange_weak
с ожидаемым = желаемым. Он берет expected
по ссылке и обновляет его, если сравнение не удается.) Но удар по строке кэша с lock cmpxchg
, вероятно, не помогает существованию ProcessA возможность быстро передать свой магазин в L1d.
Проверьте счетчик перфорации machine_clears.memory_ordering
, чтобы увидеть, происходит ли это без _mm_pause
. Если это так, то сначала попробуйте _mm_pause
, а затем, возможно, попытайтесь использовать atomic_compare_exchange_weak
в качестве нагрузки. Или atomic_fetch_add(&bDOIT, 0)
, потому что lock xadd
будет эквивалентно.
// GNU C11. The typedef in your question looks like C, redundant in C++, so I assumed C.
#include <immintrin.h>
#include <stdatomic.h>
#include <stdalign.h>
alignas(64) atomic_bool bDOIT;
typedef struct { int a,b,c,d; // 16 bytes
int e,f,g,h; // another 16
} XYZ;
alignas(64) XYZ glbXYZ;
extern void doSomething(XYZ);
// just one object (of arbitrary type) that might be modified
// maybe cheaper than a "memory" clobber (compile-time memory barrier)
#define MAYBE_MODIFIED(x) asm volatile("": "+g"(x))
// suggested ProcessB
void ProcessB(void) {
int prefetch_counter = 32; // local that doesn't escape
while(1){
if (atomic_load_explicit(&bDOIT, memory_order_acquire)){
MAYBE_MODIFIED(glbXYZ);
XYZ localxyz = glbXYZ; // or maybe a seqlock_read
// MAYBE_MODIFIED(glbXYZ); // worse code from clang, but still good with gcc, unlike a "memory" clobber which can make gcc store localxyz separately from writing it to the stack as a function arg
// asm("":::"memory"); // make sure it finishes reading glbXYZ instead of optimizing away the copy and doing it during doSomething
// localxyz hasn't escaped the function, so it shouldn't be spilled because of the memory barrier
// but if it's too big to be passed in RDI+RSI, code-gen is in practice worse
doSomething(localxyz);
} else {
if (0 == --prefetch_counter) {
// not too often: don't want to slow down writes
__builtin_prefetch(&glbXYZ, 0, 3); // PREFETCHT0 into L1d cache
prefetch_counter = 32;
}
_mm_pause(); // avoids memory order mis-speculation on bDOIT
// probably worth it for latency and throughput
// even though it pauses for ~100 cycles on Skylake and newer, up from ~5 on earlier Intel.
}
}
}
Это прекрасно компилируется на Годболте в довольно симпатичный ассм. Если bDOIT
остается верным, это узкий цикл без накладных расходов на вызов. clang7.0 даже использует SSE загрузки / сохранения, чтобы скопировать структуру в стек как функцию arg по 16 байт за раз.
Очевидно, что речь идет о беспорядке неопределенного поведения, который вы должны исправить с помощью _Atomic
(C11) или std::atomic
(C ++ 11) с помощью memory_order_relaxed
. Или mo_release
/ mo_acquire
. У вас нет какого-либо барьера памяти в функции, которая записывает bDOIT
, так что это может вывести это из цикла. * * * * * * * * * * * * * * * * * С ослабленным порядком памяти, что в буквальном смысле ноль отрицательно сказывается на качестве ассма.
Предположительно, вы используете SeqLock или что-то для защиты glbXYZ
от разрывов. Да, asm("":::"memory")
должен заставить это работать, заставляя компилятор предполагать, что он был изменен асинхронно. Ввод "g"(glbXYZ)
оператора asm бесполезен, хотя . Он глобален, поэтому барьер "memory"
уже применяется к нему (поскольку оператор asm
уже может ссылаться на него). Если вы хотите сообщить компилятору, что просто он мог бы измениться, используйте asm volatile("" : "+g"(glbXYZ));
без "memory"
clobber.
Или в C (не C ++), просто сделайте его volatile
и выполните присваивание структуры, позволяя компилятору выбрать способ его копирования без использования барьеров. В C ++ foo x = y;
терпит неудачу для volatile foo y;
, где foo
является агрегатным типом, подобным struct. volatile struct = struct невозможна, почему? . Это раздражает, когда вы хотите использовать volatile
, чтобы сообщить компилятору, что данные могут изменяться асинхронно как часть реализации SeqLock в C ++, но вы все же хотите позволить компилятору копировать его настолько эффективно, насколько это возможно в произвольном порядке, а не в узком порядке. член одновременно.
Сноска 1 : C ++ 17 определяет std::hardware_destructive_interference_size
в качестве альтернативы жесткому кодированию 64 или созданию собственной константы CLSIZE, но gcc и clang не реализуют ее тем не менее, потому что он становится частью ABI, если используется в alignas()
в структуре, и, следовательно, фактически не может меняться в зависимости от фактического размера строки L1d.