Имеет ли atomic_thread_fence(memory_order_seq_cst) семантику полного барьера памяти?
Полный / общий барьер памяти - это тот, где все операции LOAD и STORE, указанные до барьера, будут казаться выполненными до всех операций LOAD и STORE, указанных после барьера, относительно других компонентов системы.
Согласно cppreference, memory_order_seq_cst
равно memory_order_acq_rel
плюс один общий порядок изменения для всех операций, помеченных таким образом. Но, насколько я знаю, ни забор, ни освобождение в C++11 не обеспечивают порядок #StoreLoad (загрузка после сохранения). Ограничение выпуска требует, чтобы никакое предыдущее чтение / запись не могло быть переупорядочено с любой последующей записью; Ограничение получения требует, чтобы никакое последующее чтение / запись не могло быть переупорядочено с любым предыдущим чтением. Пожалуйста, поправьте меня, если я ошибаюсь;)
Приведу пример,
atomic<int> x;
atomic<int> y;
y.store(1, memory_order_relaxed); //(1)
atomic_thread_fence(memory_order_seq_cst); //(2)
x.load(memory_order_relaxed); //(3)
Разрешено ли оптимизирующему компилятору переупорядочивать инструкцию (3) в before (1), чтобы она выглядела следующим образом:
x.load(memory_order_relaxed); //(3)
y.store(1, memory_order_relaxed); //(1)
atomic_thread_fence(memory_order_seq_cst); //(2)
Если это действительная трансформация, то это доказывает, что atomic_thread_fence(memory_order_seq_cst)
не обязательно охватывает семантику того, что имеет полный барьер.
2 ответа
atomic_thread_fence(memory_order_seq_cst)
всегда создает полный барьер.
- x86_64:
MFENCE
- PowerPC:
hwsync
- Itanuim:
mf
- ARMv7 / ARMv8:
dmb ish
- MIPS64:
sync
Главное: наблюдающая нить может просто наблюдать в другом порядке, и не будет иметь значения, какие ограждения вы используете в наблюдаемой нити.
Разрешено ли оптимизирующему компилятору переупорядочивать инструкцию (3) в before (1)?
Нет, это не разрешено. Но в глобально видимой для многопоточности программе это верно, только если:
- другие темы используют то же самое
memory_order_seq_cst
для атомарных операций чтения / записи с этими значениями - или если другие потоки используют то же самое
atomic_thread_fence(memory_order_seq_cst);
между load() и store() тоже - но этот подход не гарантирует последовательную согласованность в целом, потому что последовательная согласованность является более сильной гарантией
Рабочий проект, Стандарт для языка программирования C++ 2016-07-12: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf
§ 29.3 Порядок и последовательность
§ 29,3 / 8
[Примечание: memory_order_seq_cst обеспечивает последовательную согласованность только для программы, которая не использует гонки данных и использует исключительно операции memory_order_seq_cst. Любое использование более слабого порядка приведет к аннулированию этой гарантии, если не будет применена предельная осторожность. В частности, заборы memory_order_seq_cst обеспечивают общий порядок только для самих заборов. Как правило, ограждения нельзя использовать для восстановления последовательной согласованности атомарных операций с более слабыми характеристиками упорядочения. - конец примечания]
Как это может быть отображено на ассемблере:
Случай 1:
atomic<int> x, y
y.store(1, memory_order_relaxed); //(1)
atomic_thread_fence(memory_order_seq_cst); //(2)
x.load(memory_order_relaxed); //(3)
Этот код не всегда эквивалентен значению Case-2, но этот код выдает одинаковые инструкции между STORE & LOAD, а также если используются оба типа LOAD и STORE memory_order_seq_cst
- это последовательная согласованность, которая предотвращает переупорядочение StoreLoad, вариант 2:
atomic<int> x, y;
y.store(1, memory_order_seq_cst); //(1)
x.load(memory_order_seq_cst); //(3)
С некоторыми заметками:
- он может добавить повторяющиеся инструкции (как в следующем примере для MIPS64)
или может использовать подобные операции в виде других инструкций:
- как в альтернативных 3/4 сопоставлениях для x86_64,
LOCK
Префикс сбрасывает Store-Buffer точно так же, какMFENCE
предотвратить переупорядочение StoreLoad - или ARMv8 - мы знаем, что
DMB ISH
являются полным барьером, который предотвращает переупорядочение StoreLoad: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/CHDGACJD.html
- как в альтернативных 3/4 сопоставлениях для x86_64,
Руководство по ARMv8-A
Таблица 13.1. Параметры барьера
ISH
Любой - ЛюбойЛюбой - Любой. Это означает, что как загрузка, так и хранение должны завершиться до барьера. Как загрузки, так и хранилища, которые появляются после барьера в программном порядке, должны ждать завершения барьера.
Предотвратить переупорядочение двух инструкций можно путем дополнительных инструкций между этими двумя. И как мы видим, первый STORE(seq_cst) и следующий LOAD(seq_cst) генерируют инструкции между ними так же, как FENCE (seq_cst) (atomic_thread_fence(memory_order_seq_cst)
)
Отображение C/C++11 memory_order_seq_cst
различать архитектуру процессора для: load()
, store()
, atomic_thread_fence()
:
Заметка atomic_thread_fence(memory_order_seq_cst);
всегда генерирует полный барьер:
x86_64: STORE-
MOV (into memory),
MFENCE
НАГРУЗКА-MOV (from memory)
, заборMFENCE
x86_64-alt: STORE-
MOV (into memory)
НАГРУЗКА-MFENCE
,MOV (from memory)
, заборMFENCE
x86_64-alt3: STORE-
(LOCK) XCHG
НАГРУЗКА-MOV (from memory)
, заборMFENCE
- полный барьерx86_64-alt4: STORE-
MOV (into memory)
НАГРУЗКА-LOCK XADD(0)
, заборMFENCE
- полный барьерPowerPC: МАГАЗИН
hwsync; st
НАГРУЗКА-hwsync;
ld; cmp; bc; isync
, заборhwsync
Itanium: МАГАЗИН
st.rel;
mf
НАГРУЗКА-ld.acq
, заборmf
ARMv7: МАГАЗИН
dmb ish; str;
dmb ish
НАГРУЗКА-ldr; dmb ish
, заборdmb ish
ARMv7-alt: МАГАЗИН
dmb ish; str
НАГРУЗКА-dmb ish;
ldr; dmb ish
, заборdmb ish
ARMv8(AArch32): МАГАЗИН
STL
НАГРУЗКА-LDA
, заборDMB ISH
- полный барьерARMv8 (AArch64): МАГАЗИН
STLR
НАГРУЗКА-LDAR
, заборDMB ISH
- полный барьерMIPS64: МАГАЗИН
sync; sw;
sync;
НАГРУЗКА-sync; lw; sync;
, заборsync
Описаны все сопоставления семантики C/C++11 с различными архитектурами ЦП для: load(), store(), atomic_thread_fence (): http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
Потому что последовательная согласованность предотвращает переупорядочение StoreLoad, и потому что последовательная согласованность (store(memory_order_seq_cst)
и следующий load(memory_order_seq_cst)
) генерирует инструкции между своими так же, как atomic_thread_fence(memory_order_seq_cst)
, затем atomic_thread_fence(memory_order_seq_cst)
предотвращает переупорядочение StoreLoad.
По словам Герба Саттера говорить (см о времени 45:00),std::memory_order_seq_cst
будет принудительно использовать StoreLoad, в отличие от std::memory_order_acq_rel
.
Заборы C++ не являются прямыми эквивалентами инструкций по забору ЦП, хотя они вполне могут быть реализованы как таковые. Заборы C++ являются частью модели памяти C++, которая связана с ограничениями видимости и порядка.
Учитывая, что процессоры обычно переупорядочивают операции чтения и записи и кэшируют значения локально, прежде чем они станут доступны другим ядрам или процессорам, порядок, в котором эффекты становятся видимыми для других процессоров, обычно не предсказуем.
Размышляя об этой семантике, важно думать о том, что вы пытаетесь предотвратить.
Давайте предположим, что код отображается на машинные инструкции в том виде, как они написаны, (1), затем (2), (3), и эти инструкции гарантируют, что (1) будет виден глобально до выполнения (3).
Вся цель сниппета - это общение с другим потоком. Вы не можете гарантировать, что другой поток работает на каком-либо процессоре во время выполнения этого фрагмента на нашем процессоре. Поэтому весь фрагмент может работать непрерывно, и (3) все равно будет читать любое значение, которое было в x
когда (1) был выполнен. В этом случае он неотличим от порядка выполнения (3) (1) (2).
Итак: да, это разрешенная оптимизация, потому что вы не можете увидеть разницу.