Почему GCC не использует LOAD(без ограждения) и STORE+SFENCE для последовательной согласованности?

Вот четыре подхода для создания последовательной согласованности в x86/x86_64:

  1. НАГРУЗКА (без забора) и МАГАЗИН + ЗАЩИТА
  2. НАГРУЗКА (без забора) и LOCK XCHG
  3. MFENCE + ЗАГРУЗКА и МАГАЗИН (без забора)
  4. ЗАМОК XADD(0) и МАГАЗИН (без забора)

Как написано здесь: http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html

C/C++11 Операция x86 реализация

  • Загрузить Seq_Cst: MOV (из памяти)
  • Сохранить Seq Cst: (LOCK) XCHG // альтернатива: MOV (в память),MFENCE

Примечание: существует альтернативное отображение C/C++11 на x86, которое вместо блокировки (или ограждения) хранилища Seq Cst блокирует / ограждает загрузку Seq Cst:

  • Загрузить Seq_Cst: LOCK XADD(0) // альтернатива: MFENCE,MOV (из памяти)
  • Сохранить Seq Cst: MOV (в память)

GCC 4.8.2 (GDB в x86_64) использует первый (1) подход для C++ 11-std:: memory_order_seq_cst, то есть LOAD (без забора) и STORE+MFENCE:

std::atomic<int> a;
int temp = 0;
a.store(temp, std::memory_order_seq_cst);
0x4613e8  <+0x0058>         mov    0x38(%rsp),%eax
0x4613ec  <+0x005c>         mov    %eax,0x20(%rsp)
0x4613f0  <+0x0060>         mfence

Как мы знаем, это MFENCE = LFENCE+SFENCE. Тогда этот код мы можем переписать так: LOAD(without fence) and STORE+LFENCE+SFENCE

Вопросы:

  1. Почему нам не нужно использовать LFENCE здесь до LOAD, и нужно использовать LFENCE после STORE(потому что LFENCE имеет смысл только до LOAD!)?
  2. Почему GCC не использует подход: LOAD (без забора) и STORE+SFENCE для std::memory_order_seq_cst?

4 ответа

Единственное переупорядочение x86 (для обычного доступа к памяти) заключается в том, что он потенциально может переупорядочить нагрузку, которая следует за хранилищем.

SFENCE гарантирует, что все магазины до забора завершат до всех магазинов после забора. LFENCE гарантирует, что все нагрузки до ограждения завершены до всех нагрузок после ограждения. Для обычного доступа к памяти гарантии упорядочения отдельных операций SFENCE или LFENCE уже предоставлены по умолчанию. По сути, LFENCE и SFENCE сами по себе полезны только для более слабых режимов доступа к памяти в x86.

Ни LFENCE, SFENCE, ни LFENCE + SFENCE не предотвращают переупорядочение магазина с последующей загрузкой. MFENCE делает.

Соответствующая ссылка - руководство по архитектуре Intel x86.

Рассмотрим следующий код:

#include <atomic>
#include <cstring>

std::atomic<int> a;
char b[64];

void seq() {
  /*
    movl    $0, a(%rip)
    mfence
  */
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
}

void rel() {
  /*
    movl    $0, a(%rip)
   */
  int temp = 0;
  a.store(temp, std::memory_order_relaxed);
}

Что касается атомарной переменной "a", то seq() и rel() упорядочены и атомарны в архитектуре x86, потому что:

  1. mov это атомарная инструкция
  2. MOV - устаревшая инструкция, и Intel обещает, что упорядоченная семантика памяти для устаревших инструкций будет совместима со старыми процессорами, которые всегда использовали упорядоченную семантику памяти.

Для хранения постоянного значения в атомарной переменной не требуется никаких ограничений. Ограждения есть, потому что std::memory_order_seq_cst подразумевает, что синхронизирована вся память, а не только память, которая содержит атомарную переменную.

Эффект может быть продемонстрирован следующими функциями set и get:

void set(const char *s) {
  strcpy(b, s);
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
}

const char *get() {
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
  return b;
}

strcpy - это библиотечная функция, которая может использовать более новые инструкции sse, если они доступны во время выполнения. Так как инструкции sse не были доступны на старых процессорах, не существует требований к обратной совместимости и порядок памяти не определен. Таким образом, результат strcpy в одном потоке может быть не виден напрямую в других потоках.

Вышеуказанные функции set и get используют атомарное значение для обеспечения синхронизации памяти, чтобы результат strcpy стал видимым в других потоках. Теперь значение заборов имеет значение, но порядок их внутри вызова atomic::store не имеет значения, так как заборы не нужны внутри atomic::store.

SFENCE + LFENCE не является барьером StoreLoad (MFENCE), поэтому предпосылка вопроса неверна. (См. Также мой ответ на другую версию этого же вопроса от того же пользователя. Почему SFENCE + LFENCE (или нет?) Эквивалентны MFENCE?)


  • SFENCE может пропустить (появиться раньше) более ранние грузы. (Это просто барьер StoreStore).
  • LFENCE может сдать более ранние магазины. (Грузы не могут пересекать его в любом направлении: барьер LoadLoad).
  • Загрузка может проходить через SFENCE (но магазины не могут проходить через LFENCE, поэтому это барьер LoadStore, а также барьер LoadLoad).

LFENCE + SFENCE не содержит ничего, что мешает буферизировать магазин до более поздней загрузки. MFENCE предотвращает это.

В блоге Preshing более подробно и на диаграммах объясняется, как особенными являются барьеры StoreLoad, и есть практический пример рабочего кода, демонстрирующий переупорядочение без MFENCE. Любой, кто смущен упорядочением памяти, должен начать с этого блога.

У x86 сильная модель памяти, в которой каждое нормальное хранилище имеет семантику выпуска, а каждая нормальная загрузка - семантику получения. Этот пост содержит подробности.

LFENCE и SFENCE существуют только для использования с movnt загружает / хранит, которые слабо упорядочены, а также обходят кеш.


Если эти ссылки когда-либо умрут, в моем ответе будет еще больше информации по другому подобному вопросу.

std::atomic<int>::store сопоставлен с внутренним компилятором __atomic_store_n, (Эта и другие особенности атомарных операций описаны здесь: Встроенные функции для атомарных операций, ориентированных на модель памяти.) _n суффикс делает его типо-родовым; Бэкэнд фактически реализует варианты для определенных размеров в байтах. int на x86 AFAIK всегда длиной 32 бита, так что это означает, что мы ищем определение __atomic_store_4, Внутреннее руководство для этой версии GCC говорит, что __atomic_store операции соответствуют шаблонам описания машин atomic_store‌mode; режим, соответствующий 4-байтовому целому числу, - "SI" ( это задокументировано здесь), поэтому мы ищем что-то под названием " atomic_storesi "в описании машины x86. И это приводит нас к config / i386 / sync.md, а именно к этому биту:

(define_expand "atomic_store<mode>"
  [(set (match_operand:ATOMIC 0 "memory_operand")
        (unspec:ATOMIC [(match_operand:ATOMIC 1 "register_operand")
                        (match_operand:SI 2 "const_int_operand")]
                       UNSPEC_MOVA))]
  ""
{
  enum memmodel model = (enum memmodel) (INTVAL (operands[2]) & MEMMODEL_MASK);

  if (<MODE>mode == DImode && !TARGET_64BIT)
    {
      /* For DImode on 32-bit, we can use the FPU to perform the store.  */
      /* Note that while we could perform a cmpxchg8b loop, that turns
         out to be significantly larger than this plus a barrier.  */
      emit_insn (gen_atomic_storedi_fpu
                 (operands[0], operands[1],
                  assign_386_stack_local (DImode, SLOT_TEMP)));
    }
  else
    {
      /* For seq-cst stores, when we lack MFENCE, use XCHG.  */
      if (model == MEMMODEL_SEQ_CST && !(TARGET_64BIT || TARGET_SSE2))
        {
          emit_insn (gen_atomic_exchange<mode> (gen_reg_rtx (<MODE>mode),
                                                operands[0], operands[1],
                                                operands[2]));
          DONE;
        }

      /* Otherwise use a store.  */
      emit_insn (gen_atomic_store<mode>_1 (operands[0], operands[1],
                                           operands[2]));
    }
  /* ... followed by an MFENCE, if required.  */
  if (model == MEMMODEL_SEQ_CST)
    emit_insn (gen_mem_thread_fence (operands[2]));
  DONE;
})

Не вдаваясь в подробности, большая часть этого - тело функции C, которое будет вызываться для генерации промежуточного представления " RTL " низкого уровня операции атомарного хранилища. Когда он вызывается вашим примером кода, <MODE>mode != DImode, model == MEMMODEL_SEQ_CST, а также TARGET_SSE2 это правда, поэтому он будет называть gen_atomic_store<mode>_1 а потом gen_mem_thread_fence, Последняя функция всегда генерирует mfence, (В этом файле есть код для sfence, но я считаю, что он используется только для явно закодированных _mm_sfence (от <xmmintrin.h>).)

Комментарии предполагают, что кто-то думал, что MFENCE был необходим в этом случае. Я пришел к выводу, что либо вы ошибаетесь, считая, что ограничение нагрузки не требуется, либо это пропущенная ошибка оптимизации в GCC. Это, например, не ошибка в том, как вы используете компилятор.

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