C11 Atomic Acquire / Release и x86_64 отсутствие согласованности загрузки / хранения?

10

Я борюсь с разделом 5.1.2.4 стандарта C11, в частности с семантикой Release / Acquire. Я отмечаю, что https://preshing.com/20120913/acquire-and-release-semantics/ (среди прочих) заявляет, что:

... Семантика релиза предотвращает переупорядочение памяти релиз-релиза с любой предшествующей ему операцией чтения или записи в программном порядке.

Итак, для следующего:

typedef struct test_struct
{
  _Atomic(bool) ready ;
  int  v1 ;
  int  v2 ;
} test_struct_t ;

extern void
test_init(test_struct_t* ts, int v1, int v2)
{
  ts->v1 = v1 ;
  ts->v2 = v2 ;
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
}

extern int
test_thread_1(test_struct_t* ts, int v2)
{
  int v1 ;
  while (atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v2 = v2 ;       // expect read to happen before store/release 
  v1     = ts->v1 ;   // expect write to happen before store/release 
  atomic_store_explicit(&ts->ready, true, memory_order_release) ;
  return v1 ;
}

extern int
test_thread_2(test_struct_t* ts, int v1)
{
  int v2 ;
  while (!atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v1 = v1 ;
  v2     = ts->v2 ;   // expect write to happen after store/release in thread "1"
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
  return v2 ;
}

где те выполнены:

>   in the "main" thread:  test_struct_t ts ;
>                          test_init(&ts, 1, 2) ;
>                          start thread "2" which does: r2 = test_thread_2(&ts, 3) ;
>                          start thread "1" which does: r1 = test_thread_1(&ts, 4) ;

Поэтому я ожидал бы, что поток "1" будет иметь r1 == 1, а поток "2" будет иметь r2 = 4.

Я ожидал бы этого, потому что (в соответствии с пунктами 16 и 18 раздела 5.1.2.4):

  • все (не атомарные) операции чтения и записи «секвенируются до» и, следовательно, «происходят до» атомарной записи / выпуска в потоке «1»,
  • который "inter-thread-случается-перед" атомарным чтением / приобретением в потоке "2" (когда это читает 'true'),
  • который, в свою очередь, «секвенируется до» и, следовательно, «происходит до» (не атомарного) чтения и записи (в потоке «2»).

Однако вполне возможно, что я не смог понять стандарт.

Я заметил, что код, сгенерированный для x86_64, включает в себя:

test_thread_1:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  jne    <test_thread_1>  -- while is true
  mov    %esi,0x8(%rdi)   -- (W1) ts->v2 = v2
  mov    0x4(%rdi),%eax   -- (R1) v1     = ts->v1
  movb   $0x1,(%rdi)      -- (X1) atomic_store_explicit(&ts->ready, true, memory_order_release)
  retq   

test_thread_2:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  je     <test_thread_2>  -- while is false
  mov    %esi,0x4(%rdi)   -- (W2) ts->v1 = v1
  mov    0x8(%rdi),%eax   -- (R2) v2     = ts->v2   
  movb   $0x0,(%rdi)      -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
  retq   

И при условии, что R1 и X1 происходят в таком порядке, это дает ожидаемый результат.

Но я понимаю x86_64 так, что чтение происходит по порядку с другими операциями чтения, а запись происходит по порядку с другими операциями записи, но операции чтения и записи могут не происходить друг с другом. Что подразумевает, что X1 может произойти раньше, чем R1, и даже X1, X2, W2, R1 произойдут в таком порядке - я полагаю. [Это кажется невероятно маловероятным, но если R1 были задержаны некоторыми проблемами с кешем?]

Пожалуйста: что я не понимаю?

Я отмечаю, что если я изменю загрузки / хранилища ts->readyна memory_order_seq_cst, код, сгенерированный для хранилищ:

  xchg   %cl,(%rdi)

что соответствует моему пониманию x86_64 и даст ожидаемый результат.

Крис Холл
источник
5
На x86 все обычные (не временные) хранилища имеют семантику выпуска. Intel® 64 и IA-32 Архитектуры Software Developer Руководство Volume 3 (3A, 3B, 3C и 3D): Система Руководство по программированию , 8.2.3.3 Stores Are Not Reordered With Earlier Loads. Таким образом, ваш компилятор правильно переводит ваш код (как это ни удивительно), так что ваш код фактически полностью последовательный, и ничего интересного не происходит одновременно.
EOF
Спасибо ! (Я собирался спокойно помешаться.) FWIW Я рекомендую ссылку - особенно раздел 3, «Модель программиста». Но чтобы избежать ошибки, на которую я попал, обратите внимание, что в «3.1 Абстрактной машине» есть «аппаратные потоки», каждый из которых представляет собой «один поток выполнения команд по порядку » (мой акцент добавлен). Теперь я могу вернуться к попытке понять стандарт C11 ... с меньшим когнитивным диссонансом :-)
Крис Холл,

Ответы:

1

Модель памяти x86 в основном последовательная согласованность плюс буфер хранилища (с пересылкой хранилища). Так что каждый магазин - это релиз-магазин 1 . Вот почему только seq-cst магазины нуждаются в специальных инструкциях. ( C / C ++ 11 атомных отображений в asm ). Кроме того, https://stackoverflow.com/tags/x86/info содержит некоторые ссылки на документы x86, в том числе формальное описание модели памяти x86-TSO (в основном нечитаемо для большинства людей; требует просмотра множества определений).

Поскольку вы уже читаете превосходную серию статей Джеффа Прешинга, я укажу вам еще одну, которая более подробно рассматривается: https://preshing.com/20120930/weak-vs-strong-memory-models/

Единственное изменение порядка, которое разрешено в x86, - это StoreLoad, а не LoadStore , если мы говорим в этих терминах. (Переадресация магазина может сделать дополнительные забавные вещи, если загрузка только частично перекрывает хранилище; инструкции загрузки глобально невидимым , хотя вы никогда не получите это в сгенерированном компилятором коде для stdatomic.)

@EOF прокомментировал правильную цитату из руководства Intel:

Руководство разработчика программного обеспечения для архитектур Intel® 64 и IA-32, том 3 (3A, 3B, 3C и 3D): Руководство по системному программированию, 8.2.3.3. Хранилища не переупорядочиваются при более ранних загрузках.


Сноска 1: игнорирование слабо упорядоченных магазинов NT; Вот почему вы обычно sfenceпосле NT-магазинов. Реализации C11 / C ++ 11 предполагают, что вы не используете хранилища NT. Если да, используйте _mm_sfenceперед операцией выпуска, чтобы убедиться, что она соответствует вашим хранилищам NT. (Как правило , не используйте _mm_mfence/ _mm_sfenceв других случаях ; обычно вам нужно только заблокировать переупорядочение во время компиляции. Или, конечно, просто использовать stdatomic.)

Питер Кордес
источник
Я считаю, что x86-TSO: модель строгого и полезного программиста для многопроцессорных систем x86 более читабельна, чем формальное описание, на которое вы ссылались. Но моя настоящая цель - полностью понять разделы 5.1.2.4 и 7.17.3 стандарта C11 / C18. В частности, я думаю, что я получаю Release / Acquire / Acquire + Release, но memory_order_seq_cst определяется отдельно, и я изо всех сил пытаюсь увидеть, как они все сочетаются друг с другом :-(
Крис Холл
@ChrisHall: я обнаружил, что это помогло понять, насколько точно может быть слабый acq / rel, и для этого вам нужно взглянуть на такие машины, как POWER, которые могут выполнять переупорядочение IRIW. (который seq-cst запрещает, но acq / rel нет). Будут ли две атомарные записи в разные места в разных потоках всегда рассматриваться в одном и том же порядке другими потоками? , Также Как достичь барьера StoreLoad в C ++ 11? обсуждается лишь то, как мало стандарт формально гарантирует для порядка за пределами случаев synchronizes-with или everything-seq-cst.
Питер Кордес
@ChrisHall: главное, что делает seq-cst, - это переупорядочивание StoreLoad. (На x86 это единственное, что делает кроме acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act использует asm, но это эквивалентно seq-cst против acq / rel
Питер Кордес