Сделать предыдущие хранилища памяти видимыми для последующих загрузок памяти

Я хочу хранить данные в большом массиве с _mm256_stream_si256(), вызываемым в цикле. Как я понял, забор памяти необходим, чтобы сделать эти изменения видимыми для других потоков. Описание _mm_sfence() говорит

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

Но будут ли мои последние магазины текущего потока видны последующим инструкциям load тоже (в других потоках)? Или мне нужно позвонить _mm_mfence()? (Последний кажется медленным)

ОБНОВЛЕНИЕ: Я раньше видел этот вопрос: когда следует использовать _mm_sfence _mm_lfence и _mm_mfence. Ответы там скорее сосредоточены на том, когда использовать забор в целом. Мой вопрос более конкретный, и ответы в этом вопросе вряд ли будут устранены (и в настоящее время этого не делают).

UPDATE2: следуя комментариям/ответам, определите "последующие нагрузки" как нагрузки в потоке, которые впоследствии берут блокировку, которая в настоящий момент поддерживается текущим потоком.

Ответ 1

Но будут ли мои последние магазины видны для последующих инструкций load?

Это предложение имеет мало смысла. Нагрузки - это единственный способ, которым любой поток может видеть содержимое памяти. Не уверен, почему вы говорите "слишком", так как нет ничего другого. (Кроме DMA, читается системными устройствами без ЦП.)

Определение того, что хранилище становится глобально видимым, заключается в том, что нагрузки в других потоках получат данные от него. Это означает, что хранилище оставило частный накопительный буфер процессора и является частью согласованности который включает в себя кэши данных всех ЦП. (https://en.wikipedia.org/wiki/Cache_coherence).


В обычных магазинах x86 есть освободить семантику упорядочения памяти (С++ 11 std::memory_order_release). В потоковых магазинах MOVNT есть расслабленное упорядочение, но функции mutex/spinlock и поддержка компилятора для С++ 11 std:: atomic, в основном игнорируют их. Для многопоточного кода вы должны сами их запереть, чтобы не нарушать синхронизацию функций библиотеки mutex/lock, потому что они только синхронизируют жестко упорядоченные нагрузки и хранилища x86.

Нагрузки в потоке, который выполнял магазины, все равно всегда будут видеть самое последнее сохраненное значение, даже из movnt магазинов. Вам никогда не нужны заборы в однопоточной программе. Кардинальное правило исполнения вне порядка и переупорядочение памяти состоит в том, что он никогда не нарушает иллюзию запуска в программе в рамках одного потока. То же самое для переупорядочения во время компиляции: поскольку параллельный доступ для чтения/записи к общим данным - это С++ Undefined Behavior, компиляторы должны сохранять однопоточное поведение, если вы не используете заграждения для ограничения переупорядочения времени компиляции.


MOVNT + SFENCE полезна в таких случаях, как многопоточность производителя-потребителя или с нормальной блокировкой, когда разблокировка спин-блокировки является только хранилищем-релизом.

Производственный поток пишет большой буфер с потоковыми хранилищами, а затем сохраняет "true" (или адрес буфера или что-то еще) в общую переменную флага. (Jeff Preshing называет это переменной полезной нагрузки + защитой).

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

Производитель должен использовать sfence после записи буфера, но перед тем, как писать флаг, чтобы убедиться, что все хранилища в буфере глобально видны перед флагом. (Но помните, что хранилища NT по-прежнему всегда локально видны прямо в текущем потоке.)

(С помощью функции блокировки библиотеки флагом, который хранится, является блокировка. Другие потоки, пытающиеся получить блокировку, используют нагрузку-загрузку.)

std::atomic <bool> buffer_ready;

producer() {
    for(...) {
        _mm256_stream_si256(buffer);
    }
    _mm_sfence();

    buffer_ready.store(true, std::memory_order_release);
}

В asm будет что-то вроде

 vmovntdqa [buf], ymm0
 ...
 sfence
 mov  byte [buffer_ready], 1

Без sfence некоторые из хранилищ movnt могут быть отложены до момента сохранения хранилища флагов, нарушая семантику выпуска обычного не-NT-хранилища.

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


(в комментариях) "последующим" я подразумеваю, что это происходит позже.

Невозможно сделать это, если вы не ограничены, когда эти нагрузки могут быть выполнены, используя что-то, что синхронизирует поток производителя с потребителем. Как указано, вы запрашиваете sfence, чтобы сделать хранилища NT глобально видимыми в тот момент, когда он выполняется, так что нагрузки на другие ядра, которые выполняют 1 такт после sfence, будут видеть магазины. Правильное определение "последующее" будет "в следующем потоке, который фиксирует этот поток в настоящий момент".


Задания сильнее, чем sfence, тоже:

Любая операция атомарного чтения-изменения-записи на x86 требует префикса lock, который является полным барьером памяти (например, mfence).

Итак, если вы, например, увеличиваете атомный счетчик после ваших потоковых хранилищ, вам также не нужно sfence. К сожалению, в С++ std:atomic и _mm_sfence() не знают друг о друге, а компиляторам разрешено оптимизировать атомику, следуя правилу as-if. Поэтому трудно быть уверенным в том, что инструкция lock ed RMW будет находиться именно там, где она вам нужна, в полученном asm.

(В принципе, если в абстрактной машине С++ возможно определенное упорядочение, компилятор может испускать asm, что делает это всегда так... например, сбрасывать два последовательных приращения в один +=2, чтобы нить ни разу не наблюдала, что счетчик является нечетным числом.)

Тем не менее, по умолчанию mo_seq_cst предотвращает много переупорядочивания во время компиляции, и нет недостатка в его использовании для операции чтения-изменения-записи, когда вы используете только таргетинг на x86. sfence довольно дешев, поэтому, вероятно, не стоит прилагать усилий, чтобы избежать его между некоторыми потоковыми хранилищами и операцией lock ed.

Связанный: pthreads v. SSE слабая память порядка. Об этом вопросе подумал, что разблокировка блокировки всегда будет выполнять операцию lock ed, что делает sfence избыточным.


Компиляторы С++ не пытаются вставить sfence для вас после потоковых хранилищ, даже если есть операции std::atomic с порядком более сильного, чем relaxed. Было бы слишком сложно для компиляторов надежно получить это право, не будучи очень консервативным (например, sfence в конце каждой функции с хранилищем NT, если вызывающий использует атомику).

Предшественник Intel по умолчанию C11 stdatomic и С++ 11 std::atomic.  Реализация std::atomic притворяется, что слабо упорядоченные магазины не существуют, поэтому вы должны сами их ограждать себя.

Это похоже на хороший выбор дизайна, поскольку вы хотите использовать магазины movnt в особых случаях из-за их поведения при выводе кеша. Вы не хотите, чтобы компилятор когда-либо вставлял sfence туда, где он не нужен, или используя movnti для std::memory_order_relaxed.

Ответ 2

Но мои последние магазины текущего потока будут видны последующие инструкции по загрузке тоже (в других потоках)? Или у меня есть вызвать _mm_mfence()? (Последний кажется медленным)

Ответ НЕТ. Вам не гарантированно видеть предыдущие магазины в одном потоке без каких-либо попыток синхронизации в другом потоке. Почему это?

  • Компилятор может изменить порядок команд
  • Ваш процессор может изменить порядок инструкций (на некоторых платформах).

В С++ компилятор должен испускать последовательно согласованный код, но только для однопоточного исполнения. Поэтому рассмотрим следующий код:

int x = 5;
int y = 7;
int z = x;

В этой программе компилятор может выбрать x = 5 после y = 7, но не позже, поскольку это будет непоследовательно.
Если вы затем рассмотрите следующий код в другом потоке

int a = y;
int b = x;

Такое переупорядочение команд может происходить здесь, когда a и b не зависят друг от друга. Что будет результатом запуска этих потоков?

a    b
7    5
7    ? - whatever was stored in x before the assignment of 5
...

И этот результат мы можем получить, даже если мы поместим барьер памяти между x = 5 и y = 7, потому что без помех между a = y и b = x тоже вы никогда не знаете, в каком порядке они будут прочитаны.

Это просто грубая презентация того, что вы можете прочитать в сообщении блога Джефф Прешинг Заказ памяти во время компиляции