Когда я должен использовать _mm_sfence _mm_lfence и _mm_mfence - PullRequest
13 голосов
/ 27 декабря 2010

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

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

_mm_sfence()
_mm_lfence()
_mm_mfence()

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

Ответы [ 4 ]

4 голосов
/ 12 октября 2012

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

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

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

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

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

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

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

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

2 голосов
/ 10 июня 2018

Если вы используете 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 также является полным барьером, но работает быстрее и работает в магазине.В Skylake способ реализации mfence предотвращает выполнение не по порядку даже следующих за ним инструкций без памяти.См. в нижней части этого ответа .

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

Серьезно просто используйте C ++ 11 std :: atomic или C11 stdatomic, если это возможно;Его проще использовать, и вы получаете довольно хороший код для многих вещей.Или в ядре Linux, уже есть функции-оболочки для встроенного asm для необходимых барьеров.Иногда это просто барьер компилятора, иногда это также инструкция 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), таких как видеопамять. Даже movntdqa (_mm_stream_load_si128) по-прежнему строго упорядочен в обычной (WB = обратной записи) памяти и ничего не делает для уменьшения загрязнения кэша. (prefetchnta может, но это трудно настроить и может ухудшить ситуацию.)

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

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

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

1 голос
/ 09 июня 2018

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

0 голосов
/ 29 декабря 2010

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

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

array[idx+1] = something
idx++

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

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

newestthing = array[idx]

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

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

...