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