Есть ли какой-либо барьер компилятора, который равен asm ( ":" memory") в С++ 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<> нагрузки и/или магазины, поэтому он может даже помочь избежать поведения 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'?.