В c ++, можем ли мы гарантировать случайность между двумя потоками с помощью volatile + memory fence (sfence + lfence)? - PullRequest
1 голос
/ 13 марта 2020

Короче говоря, могут ли данные, хранящиеся в src, быть правильно скопированы в dst, в следующем коде?

volatile bool flag = false;

// In thread A.
memset(mid, src, size);
__asm__ __volatile__("sfence" ::: "memory");
flag = true;

// In thread B.
while (flag == false);
__asm__ __volatile__("lfence" ::: "memory");
memset(dst, mid, size);

Ответы [ 2 ]

1 голос
/ 13 марта 2020

Если вы спрашиваете о модели памяти C ++, то ответ отрицательный, ваш код не является потокобезопасным по нескольким причинам:

  1. В модели памяти C ++ одновременный доступ к объекту из нескольких Потоки, в которых хотя бы один доступ является модификацией, составляют гонку данных , то есть UB. Единственным исключением из этого правила являются примитивы синхронизации потоков, такие как атомарность, мьютексы, условные переменные и т. Д. c. Изменчивые переменные не исключение.
  2. Доступ к переменной flag не обязательно должен быть атомом c, даже если он помечен как volatile. Это означает, что поток B может наблюдать значение, которое не было сохранено в flag, включая представление прерывания (то есть представление, которое не соответствует действительному значению bool). Использование такого значения может привести к неопределенному поведению, например, наблюдаемое значение flag может быть не равно true или false.
  3. Запись или чтение переменной volatile не составляет отношение «происходит до». Другими словами, это не ограждение компилятора или аппаратного обеспечения, которое позволяет переупорядочивать окружающий код вокруг операций чтения или записи volatile либо компилятором, либо процессором. Ваша попытка ввести забор с помощью блока asm не переносима.

На практике ваш код может выдавать последовательность инструкций x86, которые будут вести себя так, как вы ожидаете. Это было бы чистым совпадением, учитывая, что:

  1. sizeof(bool) == 1 на x86 практически во всех ОС, а хранение и загрузка байтов - это atomi c на x86. Обратите внимание, что существуют платформы, на которых sizeof(bool) > 1 и, следовательно, доступ к ним может быть не атомарным c.
  2. На x86 заказываются обычные магазины и загрузки. Другими словами, более поздние хранилища не могут быть переупорядочены ранее более ранним ЦПУ; то же самое с нагрузками. Многие другие архитектуры ЦП не так строги.
  3. Большинство компиляторов избегают переупорядочения кода вокруг изменчивых операций. Некоторые компиляторы, такие как MSV C, например, даже рассматривают энергозависимые операции как ограждения компилятора. Это не относится к g cc. К счастью, квалификатор __volatile__ не позволяет компилятору переупорядочить блок asm (и реализуемую им забор) с окружающим кодом. Это сделает блоки asm с аппаратными заборами эффективными с этим компилятором и совместимыми.

Но я повторюсь, если код работает, то только по стечению обстоятельств. Это не обязательно, даже на x86, так как компилятор может оптимизировать этот код так, как он хочет, поскольку, насколько это касается, параллелизм потоков здесь не задействован. Вы можете полагаться на гарантии, предоставляемые указанным c компилятором, такие как нестандартная семантика volatile, встроенные функции и блоки asm, но в этот момент ваша программа не является переносимой C / C ++ и написана для спецификаций * 1046. * компилятор, возможно с указанным c набором ключей командной строки.

1 голос
/ 13 марта 2020

https://gcc.gnu.org/wiki/DontUseInlineAsm

Не используйте этот код на практике, используйте std::atomic<bool> с memory_order_release и acquire, чтобы получить тот же asm code-gen (но без ненужные lfence и sfence)


Но да, это выглядит безопасным , для компиляторов, которые определяют поведение volatile таким, что UB гонки данных на volatile bool flag isn это не проблема. Это относится к компиляторам, таким как G CC, которые могут компилировать ядро ​​Linux (которое катит свою собственную атомику, используя volatile, как вы делаете).

ISO C ++ строго не требует этого Например, гипотетическая реализация может существовать на машине без согласованной общей памяти, поэтому хранилища Atomi c потребуют явной очистки. Но на практике таких реализаций нет. (Однако существуют некоторые встроенные системы, в которых volatile магазины используют разные или дополнительные инструкции для работы MMIO.)


Барьер перед магазином делает его магазином релиза, а барьер после загрузки делает это приобретением нагрузки. https://preshing.com/20120913/acquire-and-release-semantics/. Происходит до того, как может быть установлено только хранилище релизов, видимое загрузкой захвата.

Модель памяти x86 asm уже запрещает все переупорядочения, кроме StoreLoad, поэтому блокировать нужно только переупорядочение во время компиляции , Это скомпилирует в asm, это то же самое, что вы получите от использования std::atomic<bool> с mo_release и mo_acquire, за исключением тех неэффективных инструкций LFENCE и SFENCE.

C ++ Как выпуск -and-receive, достигнутый на x86 только с использованием MOV? объясняет, почему модель памяти x86 asm по крайней мере так же сильна, как acq_rel.


Инструкции sfence и lfence внутри операторов asm совершенно не имеет значения , только барьерная часть asm("" ::: "memory") необходима. https://preshing.com/20120625/memory-ordering-at-compile-time/. Переупорядочение во время компиляции должно учитывать только модель памяти C ++, но то, что выбирает компилятор, затем ограничивается моделью памяти x86. (Программный порядок + буфер хранения с пересылкой в ​​хранилище = немного сильнее, чем acq_rel)

(Оператор GNU C asm без выходных операндов неявно изменчив, поэтому я опускаю явный volatile. )

(Если вы не пытаетесь синхронизировать хранилища NT? Если это так, вам нужно только sfence, а не lfence.) Делает ли модель памяти Intel избыточность SFENCE и LFENCE? да. Memset, который внутренне использует хранилища NT, будет сам использовать sfence, чтобы сделать себя совместимым со стандартным стандартом C ++ atomics / ordering -> asm mapping , используемым в x86. Если вы используете другое отображение (например, свободно использующее хранилища NT без sfence), теоретически вы можете разбить критические секции мьютекса, если вы тоже не сверните свои собственные мьютексы. (На практике в большинстве реализаций мьютекса при взятии и выпуске используется инструкция lock ed, которая является полным барьером.)

Пустой оператор asm с клоббером памяти является своего рода броском - ваш собственный эквивалент atomic_thread_fence(std::memory_order_acquire_release) из-за модели памяти x86. atomic_thread_fence(acq_rel) скомпилирует ноль asm-инструкций, просто заблокировав переупорядочение во время компиляции.

Только seq_cst thread fence должен отправлять любые asm-инструкции для flu sh буфера хранилища и ждать, пока это произойдет, прежде чем позже. грузы. он же полный барьер (например, mfence или lock ed инструкция, например, lock add qword ptr [rsp], 0).


Не катите свою собственную атомарность, используя volatile и встроенный asm

Да, вы можете, и я надеюсь, что вы просто просили понять, как все работает.

В итоге вы сделали что-то гораздо менее эффективное, чем нужно, потому что вы использовали lfence (вне барьер выполнения заказа, который по сути бесполезен для упорядочивания памяти), а не просто барьер компилятора. И ненужные sfence.

См. Когда я должен использовать _mm_sfence _mm_lfence и _mm_mfence для в основном той же проблемы, но с использованием встроенных функций вместо встроенного asm. Обычно вам нужно только _mm_sfence() после встроенных функций NT-хранилища, и вы должны оставить mfence до компилятора с std::atomic.

Когда использовать volatile с многопоточностью? - обычно никогда; используйте std::atomic с mo_relaxed вместо volatile.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...