Когда я должен использовать _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 Например, нагрузки будут удовлетворены, и магазины станут глобально видимыми (хотя это будет реализовано по-разному, пока видимый эффект в отношении упорядочения будет "как будто", который произошел).