C11 Atomic Acquire / Release та x86_64 відсутність завантаження / зберігання узгодженості?


10

Я бореться з розділом 5.1.2.4 стандарту C11, зокрема з семантикою випуску / придбання. Зауважу, що 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? має деякі дискусії про те, наскільки мало стандартно гарантує замовлення за межами синхронізованих випадків із випадками або з усією послідовністю.
Пітер Кордес

@ChrisHall: Головне, що робить seq-cst, це блокування переупорядкування StoreLoad. (На x86 це єдине, що він робить поза acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act використовує asm, але це еквівалентно seq-cst vs. acq / rel
Пітер Кордес,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.