Ответ 1
re: ваше редактирование:
Но я не хочу использовать атомную переменную.
Почему бы и нет? Если это по соображениям производительности, используйте их с memory_order_relaxed
и atomic_signal_fence(mo_whatever)
, чтобы блокировать переупорядочение компилятора без каких-либо дополнительных затрат времени выполнения, кроме барьера компилятора, потенциально блокирующего некоторые оптимизации времени компиляции, в зависимости от окружающего кода.
Если по какой-то другой причине, возможно, atomic_signal_fence
предоставит вам код, который будет работать на вашей целевой платформе. Я подозреваю, что он заказывает не atomic<>
нагрузки и/или магазины, поэтому он может даже помочь избежать поведения Undefined поведения данных в С++.
Достаточно для чего?
Независимо от любых барьеров, если два потока одновременно запускают эту функцию, ваша программа имеет Undefined Поведение из-за одновременного доступа к переменным не atomic<>
. Таким образом, единственный способ, которым этот код может быть полезен, - это если вы говорите о синхронизации с обработчиком сигнала, который работает в одном потоке.
Это также согласуется с запросом "барьера компилятора", чтобы предотвратить переупорядочение во время компиляции, поскольку выполнение вне порядка и переупорядочение памяти всегда сохраняют поведение одного потока. Поэтому вам никогда не нужны дополнительные инструкции по барьеру, чтобы убедиться, что вы видите свои собственные операции в программном порядке, вам просто нужно остановить компилятор, переупорядочивая материал во время компиляции. См. Сообщение Jeff Preshing: Заказ памяти во время компиляции
Это atomic_signal_fence
для. Вы можете использовать его с любым std::memory_order
, точно так же как thread_fence, чтобы получить различные сильные стороны барьера и только предотвратить оптимизацию, которую вам нужно предотвратить.
...
atomic_thread_fence(memory_order_acq_rel)
вообще не генерировал никакого компилятора!
Совершенно неправильно, несколькими способами.
atomic_thread_fence
- это барьер компилятора плюс любые барьеры во время выполнения, необходимые для ограничения переупорядочения в порядке, в котором наши нагрузки/хранилища становятся видимыми для других потоков.
Я предполагаю, что вы имеете в виду, что он не выдавал никаких барьерных инструкций, когда вы смотрели на выход asm для x86. Такие инструкции, как x86 MFENCE, не являются "барьерами компилятора", они являются препятствиями во время работы и не позволяют даже переупорядочивать StoreLoad во время выполнения. (Единственное переупорядочение, которое разрешает x86. SFENCE и LFENCE нужны только при использовании слабо упорядоченных (NT) магазинов, таких как MOVNTPS
(_mm_stream_ps
).)
В слабо упорядоченном ISA, таком как ARM, thread_fence (mo_acq_rel) не является бесплатным и компилируется в инструкцию. gcc5.4 использует dmb ish
. (Посмотрите на проводник компилятора Godbolt).
Предел компилятора просто предотвращает переупорядочение во время компиляции, не обязательно предотвращая переупорядочение во время выполнения. Поэтому даже в ARM atomic_signal_fence(mo_seq_cst)
не компилируется без инструкций.
Слабый барьер позволяет компилятору сделать хранилище до B
перед хранилищем до A
, если он захочет, но gcc решил решить все еще сделать их в исходном порядке даже с thread_fence (mo_acquire) (который не следует заказывать магазины в других магазинах).
Таким образом, этот пример не проверяет, является ли что-то компилятором или нет.
Странное поведение компилятора из gcc для примера, отличного от барьера компилятора:
Смотрите этот источник + asm на Godbolt.
#include <atomic>
using namespace std;
int A,B;
void foo() {
A = 0;
atomic_thread_fence(memory_order_release);
B = 1;
//asm volatile(""::: "memory");
//atomic_signal_fence(memory_order_release);
atomic_thread_fence(memory_order_release);
A = 2;
}
Это компилируется так, как вы ожидаете: thread_fence является барьером StoreStore, поэтому A = 0 должен произойти до B = 1 и не может быть объединен с A = 2.
# clang3.9 -O3
mov dword ptr [rip + A], 0
mov dword ptr [rip + B], 1
mov dword ptr [rip + A], 2
ret
Но с gcc барьер не действует, и только конечный магазин для A присутствует в выходе asm.
# gcc6.2 -O3
mov DWORD PTR B[rip], 1
mov DWORD PTR A[rip], 2
ret
Но с atomic_signal_fence(memory_order_release)
выход gcc соответствует clang. Итак, atomic_signal_fence(mo_release)
обладает барьерным эффектом, которого мы ожидаем, но atomic_thread_fence
с чем-либо более слабым, чем seq_cst, вообще не действует как барьер компилятора.
Одна из теорий заключается в том, что gcc знает, что официально Undefined Поведение для нескольких потоков записывается в переменные не atomic<>
. Это не содержит много воды, потому что atomic_thread_fence
должен работать, если используется для синхронизации с обработчиком сигнала, он просто сильнее, чем необходимо.
BTW, с atomic_thread_fence(memory_order_seq_cst)
, получаем ожидаемый
# gcc6.2 -O3, with a mo_seq_cst barrier
mov DWORD PTR A[rip], 0
mov DWORD PTR B[rip], 1
mfence
mov DWORD PTR A[rip], 2
ret
Мы получаем это даже с одним барьером, который все равно позволит магазинам A = 0 и A = 2 происходить один за другим, поэтому компилятору разрешается объединять их через барьер. (Наблюдатели, которые не видят отдельных значений A = 0 и A = 2, являются возможным упорядочением, поэтому компилятор может решить, что всегда происходит). Однако текущие компиляторы обычно не делают такого рода оптимизацию. См. Обсуждение в конце моего ответа на Может num ++ быть атомарным для 'int num'?.