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

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

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

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

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

ОБНОВЛЕНИЕ2: следуя комментариям / ответам, давайте определим "последующие загрузки" как нагрузки в потоке, которые впоследствии принимают блокировку, которую в данный момент удерживает текущий поток.

2 ответа

Решение

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

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

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

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

Разблокировка мьютекса на x86 иногда lock add, в этом случае это полный забор для магазинов NT уже. Но если вы не можете исключить реализацию мьютекса с помощью простого хранилища, тогда вам нужно как минимум sfence,


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

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


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

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

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

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

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

std::atomic <bool> buffer_ready;

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

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

АСМ будет что-то вроде

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

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

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


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

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


Заборы сильнее чем sfence работа тоже

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

Так что если вы, например, увеличиваете атомный счетчик после потоковых хранилищ, вам также не нужно sfence, К сожалению, в C++ std:atomic а также _mm_sfence() не знают друг о друге, и компиляторам разрешено оптимизировать атомы, следуя правилу "как будто". Так что трудно быть уверенным, что lock Инструкция RMW будет находиться именно в том месте, где она вам нужна в полученном ассм.

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

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

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


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

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

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

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

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

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

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

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 Кроме того, вы никогда не знаете, в каком порядке они будут прочитаны.

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

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