Барьер памяти x86 mfence и C++
Я проверяю, как компилятор выдает инструкции для многоядерных барьеров памяти на x86_64. Ниже приведен код, который я тестирую, используя gcc_x86_64_8.3
,
std::atomic<bool> flag {false};
int any_value {0};
void set()
{
any_value = 10;
flag.store(true, std::memory_order_release);
}
void get()
{
while (!flag.load(std::memory_order_acquire));
assert(any_value == 10);
}
int main()
{
std::thread a {set};
get();
a.join();
}
Когда я использую std::memory_order_seq_cst
Я могу видеть MFENCE
Инструкция используется с любой оптимизацией -O1, -O2, -O3
, Эта инструкция гарантирует, что буферы хранилища сброшены, поэтому обновляют свои данные в кеше L1D (и используют протокол MESI, чтобы убедиться, что другие потоки могут видеть эффект).
Однако, когда я использую std::memory_order_release/acquire
без оптимизации MFENCE
Инструкция также используется, но инструкция опущена с помощью -O1, -O2, -O3
оптимизации и не видеть других инструкций, которые очищают буферы.
В случае, когда MFENCE
не используется, что обеспечивает сохранение данных буфера хранилища в кэш-памяти для обеспечения семантики порядка памяти?
Ниже приведен код сборки для функций get / set с -O3
, как то, что мы получаем в проводнике компилятора Godbolt:
set():
mov DWORD PTR any_value[rip], 10
mov BYTE PTR flag[rip], 1
ret
.LC0:
.string "/tmp/compiler-explorer-compiler119218-62-hw8j86.n2ft/example.cpp"
.LC1:
.string "any_value == 10"
get():
.L8:
movzx eax, BYTE PTR flag[rip]
test al, al
je .L8
cmp DWORD PTR any_value[rip], 10
jne .L15
ret
.L15:
push rax
mov ecx, OFFSET FLAT:get()::__PRETTY_FUNCTION__
mov edx, 17
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
call __assert_fail
2 ответа
Модель упорядочения памяти x86 обеспечивает барьеры #StoreStore и #LoadStore для всех инструкций хранилища1, и это все, что требуется в семантике выпуска. Также процессор выполнит инструкцию сохранения как можно скорее; когда инструкция сохранения удаляется, хранилище становится самым старым в буфере хранения, ядро имеет целевую строку кэша в состоянии когерентности, доступной для записи, и порт кэша доступен для выполнения операции2 сохранения. Таким образом, нет необходимости в MFENCE
инструкция. Флаг станет видимым для другого потока как можно скорее, и когда это произойдет, any_value
гарантированно будет 10.
С другой стороны, последовательная согласованность также требует барьеров #StoreLoad и #LoadLoad. MFENCE
Требуется предоставить оба барьера, поэтому он используется на всех уровнях оптимизации.
Связанный: Размер буферов магазина на оборудовании Intel? Что именно является буфером магазина?,
Примечания:
(1) Есть исключения, которые здесь не применяются. В частности, временные хранилища и хранилища в не кэшируемых типах памяти, сочетающих запись, обеспечивают только барьер #LoadStore. В любом случае, эти барьеры предусмотрены для магазинов с типом памяти обратной записи на процессорах Intel и AMD.
(2) Это в отличие от хранилищ, сочетающих запись, которые сделаны глобально видимыми при определенных условиях. См. Раздел 11.3.1 руководства Intel, том 3.
(3) Смотрите обсуждение под ответом Петра.
Модель памяти x86 TSO - последовательная согласованность + буфер хранилища, поэтому только хранилищам seq-cst требуется специальное ограждение. (Остановка после хранилища до тех пор, пока буфер хранилища не опустеет, до последующей загрузки - это все, что нам нужно для восстановления последовательной согласованности). Более слабая модель acq/rel совместима с переупорядочением StoreLoad, вызванным буфером хранилища.
(См. Обсуждение в комментариях относительно того, является ли "разрешение переупорядочения StoreLoad" точным и достаточным описанием того, что позволяет x86. Ядро всегда видит свои собственные хранилища в программном порядке, потому что загружает отслеживание буфера хранилища, так что вы можете сказать, что пересылка хранилища также переупорядочивает множество недавно сохраненных данных. За исключением случаев, когда вы не всегда можете: глобально невидимые инструкции по загрузке)
(И кстати, компиляторы, кроме gcc, используют xchg
сделать последовательный магазин. Это на самом деле более эффективно на современных процессорах. ССЗ mov
+ mfence
возможно, в прошлом было дешевле, но в настоящее время обычно хуже, даже если вас не волнует старая ценность. См. Почему хранилище std::atomic с последовательной последовательностью использует XCHG? для сравнения между GCC mov+mfence
против xchg
, Также мой ответ о том, какой барьер лучше писать на x86: lock+addl или xchgl?)
Интересный факт: вы можете добиться последовательной согласованности, вместо этого ограждая загрузку seq-cst вместо хранилищ. Но для большинства случаев дешевые грузы гораздо ценнее, чем дешевые магазины, поэтому каждый использует ABI, где на полках стоят все барьеры.
См. https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html чтобы узнать, как атомарные операции C++11 отображаются в последовательности команд asm для x86, PowerPC, ARMv7, ARMv8 и Itanium. Также, когда требуются инструкции x86 LFENCE, SFENCE и MFENCE?
когда я использую std::memory_order_release/acqu без оптимизации, также используется инструкция MFENCE
Это потому что flag.store(true, std::memory_order_release);
не встроен, потому что вы отключили оптимизацию. Это включает в себя вставку очень простых функций-членов, таких как ``atomic::store(T, std::memory_order = std::memory_order_seq_cst)`
Когда параметр заказа в __atomic_store_n()
Встроенная в GCC переменная времени выполнения (в atomic::store()
реализация библиотеки), GCC проигрывает это консервативно и продвигает его в seq_cst. На самом деле это может стоить того, чтобы gcc разветвлялся mfence
потому что это так дорого, но это не то, что мы получаем.