Барьер памяти x86 mfence и C++

Я проверяю, как компилятор выдает инструкции для многоядерных барьеров памяти на x86_64. Ниже приведен код, который я тестирую, используя gcc_x86_64_8.3,

std::atomic<bool> flag {false};
int any_value {0};

void set()
{
  any_value = 10;
  flag.store(true, std::memory_order_release);
}

void get()
{
  while (!flag.load(std::memory_order_acquire));
  assert(any_value == 10);
}

int main()
{
  std::thread a {set};
  get();
  a.join();
}

Когда я использую std::memory_order_seq_cstЯ могу видеть MFENCE Инструкция используется с любой оптимизацией -O1, -O2, -O3, Эта инструкция гарантирует, что буферы хранилища сброшены, поэтому обновляют свои данные в кеше L1D (и используют протокол MESI, чтобы убедиться, что другие потоки могут видеть эффект).

Однако, когда я использую std::memory_order_release/acquire без оптимизации MFENCE Инструкция также используется, но инструкция опущена с помощью -O1, -O2, -O3 оптимизации и не видеть других инструкций, которые очищают буферы.

В случае, когда MFENCE не используется, что обеспечивает сохранение данных буфера хранилища в кэш-памяти для обеспечения семантики порядка памяти?

Ниже приведен код сборки для функций get / set с -O3, как то, что мы получаем в проводнике компилятора Godbolt:

set():
        mov     DWORD PTR any_value[rip], 10
        mov     BYTE PTR flag[rip], 1
        ret


.LC0:
        .string "/tmp/compiler-explorer-compiler119218-62-hw8j86.n2ft/example.cpp"
.LC1:
        .string "any_value == 10"

get():
.L8:
        movzx   eax, BYTE PTR flag[rip]
        test    al, al
        je      .L8
        cmp     DWORD PTR any_value[rip], 10
        jne     .L15
        ret
.L15:
        push    rax
        mov     ecx, OFFSET FLAT:get()::__PRETTY_FUNCTION__
        mov     edx, 17
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:.LC1
        call    __assert_fail

2 ответа

Решение

Модель упорядочения памяти x86 обеспечивает барьеры #StoreStore и #LoadStore для всех инструкций хранилища1, и это все, что требуется в семантике выпуска. Также процессор выполнит инструкцию сохранения как можно скорее; когда инструкция сохранения удаляется, хранилище становится самым старым в буфере хранения, ядро ​​имеет целевую строку кэша в состоянии когерентности, доступной для записи, и порт кэша доступен для выполнения операции2 сохранения. Таким образом, нет необходимости в MFENCE инструкция. Флаг станет видимым для другого потока как можно скорее, и когда это произойдет, any_value гарантированно будет 10.

С другой стороны, последовательная согласованность также требует барьеров #StoreLoad и #LoadLoad. MFENCE Требуется предоставить оба барьера, поэтому он используется на всех уровнях оптимизации.

Связанный: Размер буферов магазина на оборудовании Intel? Что именно является буфером магазина?,


Примечания:

(1) Есть исключения, которые здесь не применяются. В частности, временные хранилища и хранилища в не кэшируемых типах памяти, сочетающих запись, обеспечивают только барьер #LoadStore. В любом случае, эти барьеры предусмотрены для магазинов с типом памяти обратной записи на процессорах Intel и AMD.

(2) Это в отличие от хранилищ, сочетающих запись, которые сделаны глобально видимыми при определенных условиях. См. Раздел 11.3.1 руководства Intel, том 3.

(3) Смотрите обсуждение под ответом Петра.

Модель памяти x86 TSO - последовательная согласованность + буфер хранилища, поэтому только хранилищам seq-cst требуется специальное ограждение. (Остановка после хранилища до тех пор, пока буфер хранилища не опустеет, до последующей загрузки - это все, что нам нужно для восстановления последовательной согласованности). Более слабая модель acq/rel совместима с переупорядочением StoreLoad, вызванным буфером хранилища.

(См. Обсуждение в комментариях относительно того, является ли "разрешение переупорядочения StoreLoad" точным и достаточным описанием того, что позволяет x86. Ядро всегда видит свои собственные хранилища в программном порядке, потому что загружает отслеживание буфера хранилища, так что вы можете сказать, что пересылка хранилища также переупорядочивает множество недавно сохраненных данных. За исключением случаев, когда вы не всегда можете: глобально невидимые инструкции по загрузке)

(И кстати, компиляторы, кроме gcc, используют xchg сделать последовательный магазин. Это на самом деле более эффективно на современных процессорах. ССЗ mov + mfence возможно, в прошлом было дешевле, но в настоящее время обычно хуже, даже если вас не волнует старая ценность. См. Почему хранилище std::atomic с последовательной последовательностью использует XCHG? для сравнения между GCC mov+mfence против xchg, Также мой ответ о том, какой барьер лучше писать на x86: lock+addl или xchgl?)

Интересный факт: вы можете добиться последовательной согласованности, вместо этого ограждая загрузку seq-cst вместо хранилищ. Но для большинства случаев дешевые грузы гораздо ценнее, чем дешевые магазины, поэтому каждый использует ABI, где на полках стоят все барьеры.

См. https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html чтобы узнать, как атомарные операции C++11 отображаются в последовательности команд asm для x86, PowerPC, ARMv7, ARMv8 и Itanium. Также, когда требуются инструкции x86 LFENCE, SFENCE и MFENCE?


когда я использую std::memory_order_release/acqu без оптимизации, также используется инструкция MFENCE

Это потому что flag.store(true, std::memory_order_release); не встроен, потому что вы отключили оптимизацию. Это включает в себя вставку очень простых функций-членов, таких как ``atomic::store(T, std::memory_order = std::memory_order_seq_cst)`

Когда параметр заказа в __atomic_store_n() Встроенная в GCC переменная времени выполненияatomic::store() реализация библиотеки), GCC проигрывает это консервативно и продвигает его в seq_cst. На самом деле это может стоить того, чтобы gcc разветвлялся mfence потому что это так дорого, но это не то, что мы получаем.

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