Есть ли какой-либо барьер компилятора, который равен asm(""::: "memory") в C++11?
Мой тестовый код, как показано ниже, и я обнаружил, что только memory_order_seq_cst
Запрет на повторный заказ компилятора.
#include <atomic>
using namespace std;
int A, B = 1;
void func(void) {
A = B + 1;
atomic_thread_fence(memory_order_seq_cst);
B = 0;
}
И другие варианты, такие как memory_order_release
, memory_order_acq_rel
не создавал никакого барьера компилятора вообще.
Я думаю, что они должны работать с атомарной переменной, как показано ниже.
#include <atomic>
using namespace std;
atomic<int> A(0);
int B = 1;
void func(void) {
A.store(B+1, memory_order_release);
B = 0;
}
Но я не хочу использовать атомную переменную. В то же время, я думаю, что "asm("":::"memory")" слишком низкий уровень.
Есть ли лучший выбор?
1 ответ
Re: ваше редактирование:
Но я не хочу использовать атомную переменную.
Почему бы и нет? Если это по причинам производительности, используйте их с memory_order_relaxed
а также atomic_signal_fence(mo_whatever)
блокировать переупорядочение компилятора без каких-либо накладных расходов во время выполнения, кроме барьера компилятора, потенциально блокирующего некоторые оптимизации во время компиляции, в зависимости от окружающего кода.
Если это по какой-то другой причине, то, возможно, atomic_signal_fence
даст вам код, который работает на вашей целевой платформе. Я подозреваю, что это делает заказ не atomic<>
загружает и / или хранит, так что это может даже помочь избежать гонки данных неопределенного поведения в C++.
Достаточно для чего?
Независимо от каких-либо барьеров, если два потока запускают эту функцию одновременно, ваша программа имеет неопределенное поведение из-за одновременного доступа к atomic<>
переменные. Таким образом, этот код может быть полезен только в том случае, если вы говорите о синхронизации с обработчиком сигнала, который выполняется в том же потоке.
Это также согласуется с запросом о "барьере компилятора", чтобы предотвратить переупорядочение только во время компиляции, потому что неупорядоченное выполнение и переупорядочение памяти всегда сохраняют поведение одного потока. Таким образом, вам никогда не нужны дополнительные барьерные инструкции, чтобы убедиться, что вы видите свои собственные операции в программном порядке, вам просто нужно остановить процесс переупорядочения компилятора во время компиляции. Посмотрите пост Джеффа Прешинга: Упорядочение памяти во время компиляции
Это то, что atomic_signal_fence
для. Вы можете использовать его с любым std::memory_order
так же, как thread_fence, чтобы получить различные преимущества барьера и предотвратить только те оптимизации, которые вам нужно предотвратить.
...
atomic_thread_fence(memory_order_acq_rel)
не создавал никакого барьера компилятора вообще!
Совершенно неправильно, несколькими способами.
atomic_thread_fence
это барьер компилятора, плюс любые барьеры времени выполнения, необходимые для ограничения переупорядочения в том порядке, в котором наши загрузки / хранилища становятся видимыми для других потоков.
Я предполагаю, что вы имеете в виду, что он не испускал никаких барьерных инструкций, когда вы смотрели вывод asm для x86. Такие инструкции, как MFENCE для x86, не являются "барьерами компилятора", они являются барьерами памяти во время выполнения и предотвращают даже переупорядочивание 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;
}
Это компилируется с clang так, как вы ожидаете: 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 барьер не имеет никакого эффекта, и в выходных данных asm присутствует только окончательное хранилище для A.
# 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 знает, что это официально неопределенное поведение для нескольких потоков для записи в atomic<>
переменные. Это не держит много воды, потому что atomic_thread_fence
должен работать, если используется для синхронизации с обработчиком сигнала, он просто сильнее, чем необходимо.
Кстати, с 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'?,