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

Я прочитал "Руководство по оптимизации Intel для архитектуры Intel".

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

_mm_sfence()
_mm_lfence()
_mm_mfence()

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

Ответ 1

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

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

array[idx+1] = something
idx++

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

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

newestthing = array[idx]

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

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

Ответ 2

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

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

Отсюда я говорю о x86, x86 сильно упорядочен.

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

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

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

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

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

Ответ 3

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

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

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

lfence

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

sfence

sfence документируется для заказа магазинов до и после таким же образом, lfence и при загрузке, но так же, как и загрузки, заказ магазина уже в большинстве случаев гарантирован Intel. Основным интересным случаем, когда это не так, являются так называемые movntdq магазины, такие как 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 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 flag
test  eax, eax
jnz   retry
; critical section

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


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

Ответ 4

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

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


x86 имеет сильно упорядоченную модель памяти, но C++ имеет очень слабую модель памяти (то же самое для C). Для семантики получения/выпуска вам нужно только предотвратить переупорядочение во время компиляции. См. Статью Jeff Preshing Memory Ordering at Compile Time.

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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


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

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

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

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