Имеет ли 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)

С некоторыми заметками:

  1. он может добавить повторяющиеся инструкции (как в следующем примере для MIPS64)
  2. или может использовать подобные операции в виде других инструкций:

    • как в альтернативных 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

Руководство по 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).

Итак: да, это разрешенная оптимизация, потому что вы не можете увидеть разницу.

Другие вопросы по тегам