Когда я должен использовать _mm_sfence _mm_lfence и _mm_mfence

I read the "Intel Optimization guide Guide For Intel Architecture".

Тем не менее, я до сих пор не знаю, когда я должен использовать

_mm_sfence()
_mm_lfence()
_mm_mfence()

Может ли кто-нибудь объяснить, когда они должны использоваться при написании многопоточного кода?

4 ответа

Решение

Предостережение: я не эксперт в этом. Я все еще пытаюсь научиться этому сам. Но так как никто не ответил в последние два дня, кажется, что эксперты по инструкциям по забору памяти не являются многочисленными. Так вот мое понимание...

Intel является слабо упорядоченной системой памяти. Это означает, что ваша программа может выполняться

array[idx+1] = something
idx++

но изменение в idx может быть видимо глобально (например, для потоков / процессов, работающих на других процессорах) до изменения в массиве. Размещение sfence между двумя утверждениями обеспечит порядок отправки записей в ФСБ.

Между тем, другой процессор работает

newestthing = array[idx]

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

Эта статья или эта статья может дать лучшую информацию

Если вы используете NT магазины, вы можете захотеть _mm_sfence или, может быть, даже _mm_mfence, Варианты использования для _mm_lfence гораздо более неясны.

Если нет, просто используйте C++11 std::atomic и дайте компилятору позаботиться о деталях asm управления упорядочением памяти.


x86 имеет строго упорядоченную модель памяти, но C++ имеет очень слабую модель памяти (то же самое для C). Для получения / выпуска семантики вам нужно только предотвратить переупорядочение во время компиляции. См. Статью " Упорядочение памяти Джеффа Прешинга во время компиляции".

_mm_lfence а также _mm_sfence имеют необходимый эффект барьера компилятора, но они также заставят компилятор испустить бесполезный lfence или же sfence Инструкция asm, которая заставляет ваш код работать медленнее.

Существуют лучшие варианты для управления переупорядочением во время компиляции, когда вы не делаете ничего непонятного, что могло бы вас заинтересовать sfence,

Например, GNU C/C++ asm("" ::: "memory") является барьером компилятора (все значения должны находиться в памяти, соответствующей абстрактной машине из-за "memory" clobber), но инструкции asm не выдаются.

Если вы используете C++11 std::atomic, вы можете просто сделать shared_var.store(tmp, std::memory_order_release), Это гарантированно станет глобально видимым после любых более ранних присваиваний Си, даже неатомарным переменным.

_mm_mfence потенциально полезно, если вы катите свою собственную версию C11 / C++11 std::atomic потому что фактический mfence инструкция является одним из способов получения последовательной согласованности, то есть, чтобы последующие загрузки не читали значения до тех пор, пока предыдущие хранилища не станут глобально видимыми. См. Перестановка памяти Джеффа Прешинга, пойманная в законе.

Но учтите, что mfence кажется, медленнее на текущем оборудовании, чем использование заблокированной операции атомарного RMW. например xchg [mem], eax Это также полный барьер, но работает быстрее, и делает магазин. На Скайлэйк, кстати mfence Реализовано предотвращение неупорядоченного выполнения даже следующих за ним инструкций без памяти. Смотрите в нижней части этого ответа.

Однако в C++ без встроенного asm ваши параметры для барьеров памяти более ограничены ( Сколько инструкций для барьеров памяти имеет процессор x86?). mfence это не страшно, и это то, что gcc и clang в настоящее время используют для создания хранилищ последовательной согласованности.

Серьезно, просто используйте C++11 std::atomic или C11 stdatomic, если это возможно; Его проще использовать, и вы получаете довольно хороший код для многих вещей. Или в ядре Linux уже есть функции-оболочки для встроенного ассема для необходимых барьеров. Иногда это просто барьер компилятора, иногда это также инструкция asm, чтобы получить более сильный порядок выполнения, чем по умолчанию. (например, для полного барьера).


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


_mm_sfence безусловно, наиболее вероятный барьер для фактического использования вручную в C++

Основной вариант использования для _mm_sfence() после некоторого _mm_stream сохраняет перед установкой флага, который будут проверять другие потоки.

Посмотрите Enhanced REP MOVSB ​​для memcpy для получения дополнительной информации о хранилищах NT и обычных хранилищах и пропускной способности памяти x86. Для записи очень больших буферов (больше чем размер кэша L3), которые определенно не будут перечитаны в ближайшее время, может быть хорошей идеей использовать хранилища NT.

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

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

#include <atomic>
#include <immintrin.h>

struct bigbuf {
    int buf[100000];
    std::atomic<unsigned> buf_ready;
};

void producer(bigbuf *p) {
  __m128i *buf = (__m128i*) (p->buf);

  for(...) {
     ...
     _mm_stream_si128(buf,   vec1);
     _mm_stream_si128(buf+1, vec2);
     _mm_stream_si128(buf+2, vec3);
     ...
  }

  _mm_sfence();    // All weakly-ordered memory shenanigans stay above this line
  // So we can safely use normal std::atomic release/acquire sync for buf
  p->buf_ready.store(1, std::memory_order_release);
}

Тогда потребитель может безопасно сделать if(p->buf_ready.load(std::memory_order_acquire)) { foo = p->buf[0]; ... } без какой-либо гонки данных неопределенное поведение. Читатель сторона не нуждается _mm_lfence; Слабоупорядоченная природа NT-хранилищ ограничена только тем, что пишет ядро. Как только он становится видимым в глобальном масштабе, он становится полностью связным и упорядоченным в соответствии с обычными правилами.

Другие варианты использования включают заказ clflushopt контролировать порядок хранения данных в энергонезависимом хранилище с отображением в памяти. (например, сейчас существует NVDIMM, использующий память Optane, или модули DIMM с DRAM с резервным питанием от батареи.)


_mm_lfence почти никогда не используется в качестве фактического ограждения. Нагрузки могут быть упорядочены только слабо при загрузке из областей памяти WC (Write-Combining), таких как видео-RAM Четное movntdqa (_mm_stream_load_si128) по-прежнему строго упорядочен в обычной (WB = обратной записи) памяти и ничего не делает для уменьшения загрязнения кэша. (prefetchnta может, но это трудно настроить и может сделать вещи хуже.)

TL: DR: если вы не пишете графические драйверы или что-то еще, что напрямую отображает видеопамять, вам не нужно _mm_lfence заказать ваши грузы.

lfence действительно имеет интересный микроархитектурный эффект предотвращения выполнения более поздних инструкций, пока он не уйдет в отставку. например, чтобы остановить _rdtsc() от чтения счетчика циклов, пока более ранняя работа еще не завершена в микробенчмарке. (Применяется всегда на процессорах Intel, но на AMD только с настройкой MSR: сериализуется ли LFENCE на процессорах AMD? lfence работает 4 раза в сутки на семействе бульдозеров, поэтому явно не сериализуется.)

Поскольку вы используете встроенные функции из C/C++, компилятор генерирует код для вас. У вас нет прямого контроля над Asm, но вы можете использовать _mm_lfence для таких вещей, как смягчение Спектра, если вы можете заставить компилятор поместить его в нужное место в выводе asm: сразу после условного перехода, перед доступом к двойному массиву. (лайк foo[bar[i]]). Если вы используете исправления ядра для Spectre, я думаю, что ядро ​​защитит ваш процесс от других процессов, так что вам нужно беспокоиться об этом только в программе, которая использует изолированную программную среду JIT и беспокоится о том, что ее могут атаковать изнутри. песочница.

Вот мое понимание, надеюсь, точное и достаточно простое, чтобы иметь смысл:

(Itanium) Архитектура IA64 позволяет выполнять чтение и запись в память в любом порядке, поэтому изменение порядка памяти с точки зрения другого процессора невозможно предсказать, если только вы не используете заборы для принудительного выполнения записи в разумном порядке.

С этого момента я говорю о x86, x86 строго упорядочен.

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

Заблокированные инструкции чтения / изменения / записи полностью последовательны. Из-за этого, как правило, вы уже обрабатываете пропущенные операции памяти другого процессора, потому что заблокирован xchg или же cmpxchg синхронизирует все это, вы немедленно приобретете соответствующую строку кэша для владельца и обновите ее атомарно. Если другой процессор работает с вашей заблокированной операцией, либо вы выиграете гонку, а другой процессор пропустит кэш и вернет его после заблокированной операции, либо они выиграют гонку, и вы пропустите кэш и получите обновленную версию. ценность от них.

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

Любые заборы редко нужны в x86, они не нужны, если вы не используете объединяющую запись память или не временные инструкции, что вы редко делаете, если вы не являетесь разработчиком режима ядра (драйвера). Обычно x86 гарантирует, что все хранилища видны в программном порядке, но не дает такой гарантии для памяти WC (комбинирование записи) или для "невременных" инструкций, которые делают явные слабо упорядоченные хранилища, такие как movnti,

Подводя итог, можно сказать, что хранилища всегда отображаются в программном порядке, если только вы не использовали специальные слабо упорядоченные хранилища или не обращаетесь к типу памяти WC. Алгоритмы, использующие заблокированные инструкции, такие как xchg, или же xadd, или же cmpxchgи т. д. будут работать без заборов, потому что заблокированные инструкции последовательно согласованы.

Внутренние звонки, которые вы упоминаете, просто вставьте sfence, lfence или же mfence инструкция, когда они называются. Таким образом, возникает вопрос: "Каковы цели этих инструкций по забору"?

Краткий ответ: lfence совершенно бесполезно * и sfence почти полностью бесполезен для целей упорядочения памяти для программ пользовательского режима в x86. С другой стороны, mfence служит барьером для полной памяти, так что вы можете использовать его в местах, где вам нужен барьер, если поблизости его нет lock -приставка с указанием того, что вам нужно.

Более длинный, но все же короткий ответ...

lfence

lfence задокументировано, чтобы заказать грузы до lfence в отношении нагрузок после, но эта гарантия уже предоставляется для нормальных нагрузок без каких-либо ограничений: то есть Intel уже гарантирует, что "нагрузки не переупорядочиваются с другими нагрузками". На практике это оставляет целью lfence в коде пользовательского режима в качестве барьера выполнения не по порядку, что может быть полезно для аккуратной синхронизации определенных операций.

sfence

sfence задокументировано, чтобы заказать магазины до и после так же, как lfence делает для нагрузок, но так же, как загрузки, заказ магазина уже гарантирован в большинстве случаев Intel. Основной интересный случай, когда это не так, это так называемые временные магазины, такие как movntdq, movnti, maskmovq и несколько других инструкций. Эти инструкции не воспроизводятся по обычным правилам упорядочения памяти, поэтому вы можете поставить sfence между этими магазинами и любыми другими магазинами, где вы хотите навязать относительный порядок. mfence тоже работает для этой цели, но sfence быстрее.

mfence

В отличие от двух других, mfence на самом деле что-то делает: он служит полным барьером памяти, гарантируя, что все предыдущие загрузки и хранилища будут завершены 1 до того, как начнется выполнение любой из последующих загрузок или хранилищ. Этот ответ слишком короткий, чтобы полностью объяснить концепцию барьера памяти, но примером может служить алгоритм Деккера, где каждый поток, желающий войти в критическую секцию, сохраняет данные в определенном месте, а затем проверяет, сохранил ли другой поток что-то для его место нахождения. Например, в потоке 1:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

Здесь, на x86, вам нужен барьер памяти между хранилищами (первый mov) и нагрузка (вторая mov), иначе каждый поток мог бы видеть ноль при чтении флага другого, потому что модель памяти x86 позволяет переупорядочивать нагрузки с более ранними хранилищами. Таким образом, вы могли бы вставить mfence Барьер следующим образом для восстановления последовательной согласованности и правильного поведения алгоритма:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mfence
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

На практике вы не видите mfence столько, сколько вы могли бы ожидать, потому что инструкции с префиксом x86 имеют тот же эффект полного барьера, и они часто / всегда (?) дешевле, чем mfence,


1 Например, нагрузки будут удовлетворены, и магазины станут глобально видимыми (хотя это будет реализовано по-разному, пока видимый эффект в отношении упорядочения будет "как будто", который произошел).

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