C11 Atomic Acquire / Release dan kurangnya x86_64 koherensi load / store?

10

Saya berjuang dengan Bagian 5.1.2.4 dari Standar C11, khususnya semantik Release / Acquire. Saya perhatikan bahwa https://preshing.com/20120913/acquire-and-release-semantics/ (antara lain) menyatakan bahwa:

... Rilis semantik mencegah penyusunan ulang memori dari rilis-rilis dengan operasi baca atau tulis apa pun yang mendahuluinya dalam urutan program.

Jadi, untuk yang berikut ini:

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 ;
}

di mana mereka dieksekusi:

>   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) ;

Karena itu, saya berharap utas "1" memiliki r1 == 1 dan utas "2" memiliki r2 = 4.

Saya mengharapkan itu karena (mengikuti paragraf 16 dan 18 dari sekte 5.1.2.4):

  • semua (bukan atom) membaca dan menulis adalah "diurutkan sebelum" dan karenanya "terjadi sebelum" atom menulis / melepaskan di utas "1",
  • yang "inter-thread-terjadi-sebelum" atom membaca / memperoleh di utas "2" (ketika dibaca 'benar'),
  • yang pada gilirannya "diurutkan sebelum" dan karenanya "terjadi sebelum" (bukan atom) membaca dan menulis (di utas "2").

Namun, sangat mungkin bahwa saya gagal memahami standar.

Saya amati bahwa kode yang dihasilkan untuk x86_64 termasuk:

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   

Dan asalkan R1 dan X1 terjadi dalam urutan itu, ini memberikan hasil yang saya harapkan.

Tetapi pemahaman saya tentang x86_64 adalah bahwa pembacaan terjadi secara berurutan dengan pembacaan lain dan penulisan terjadi sesuai dengan penulisan lainnya, tetapi pembacaan dan penulisan mungkin tidak terjadi secara berurutan. Yang menyiratkan adalah mungkin untuk X1 terjadi sebelum R1, dan bahkan untuk X1, X2, W2, R1 terjadi dalam urutan itu - saya percaya. [Ini sepertinya sangat tidak mungkin, tetapi jika R1 ditahan oleh beberapa masalah cache?]

Tolong: apa yang tidak saya mengerti?

Saya perhatikan bahwa jika saya mengubah beban / toko ts->readyke memory_order_seq_cst, kode yang dihasilkan untuk toko adalah:

  xchg   %cl,(%rdi)

yang konsisten dengan pemahaman saya tentang x86_64 dan akan memberikan hasil yang saya harapkan.

Chris Hall
sumber
5
Pada x86, semua toko biasa (bukan non temporal) memiliki semantik rilis. Intel® 64 dan IA-32 Arsitektur Software Developer Manual Volume 3 (3A, 3B, 3C & 3D): Panduan Sistem Pemrograman , 8.2.3.3 Stores Are Not Reordered With Earlier Loads. Jadi kompiler Anda menerjemahkan kode Anda dengan benar (betapa mengejutkannya), sehingga kode Anda secara efektif benar-benar berurutan dan tidak ada yang menarik terjadi secara bersamaan.
EOF
Terima kasih ! (Saya akan diam-diam gila.) FWIW Saya merekomendasikan tautan - terutama bagian 3, "Modelmer's Model". Namun untuk menghindari kesalahan aku jatuh ke dalam, catatan bahwa dalam "3.1 Abstrak Machine" ada "benang hardware" yang masing-masing adalah "satu di-order aliran eksekusi instruksi" (penekanan saya tambah). Sekarang saya dapat kembali mencoba memahami Standar C11 ... dengan disonansi kognitif yang lebih sedikit :-)
Chris Hall

Jawaban:

1

model memori x86 pada dasarnya adalah konsistensi sekuensial ditambah buffer toko (dengan penerusan toko). Jadi setiap toko adalah toko rilis 1 . Inilah sebabnya mengapa hanya toko seq-cst yang memerlukan instruksi khusus. ( C / C ++ 11 pemetaan atom ke asm ). Selain itu, https://stackoverflow.com/tags/x86/info memiliki beberapa tautan ke x86 docs, termasuk deskripsi formal dari model memori x86-TSO (pada dasarnya tidak dapat dibaca oleh kebanyakan manusia, membutuhkan penjelajahan melalui banyak definisi).

Karena Anda sudah membaca serangkaian artikel luar biasa Jeff Preshing, saya akan mengarahkan Anda ke artikel lain yang lebih detail: https://preshing.com/20120930/weak-vs-strong-memory-models/

Satu-satunya pemesanan ulang yang diizinkan pada x86 adalah StoreLoad, bukan LoadStore , jika kita berbicara dalam istilah-istilah itu. (Penerusan toko dapat melakukan hal-hal menyenangkan tambahan jika memuat hanya tumpang tindih sebagian toko; Secara global instruksi memuat tak terlihat , meskipun Anda tidak akan pernah mendapatkannya dalam kode yang dibuat oleh compiler stdatomic.)

@EOF berkomentar dengan kutipan yang tepat dari manual Intel:

Manual Pengembang Perangkat Lunak Arsitektur Perangkat Lunak Intel® 64 dan IA-32 Volume 3 (3A, 3B, 3C & 3D): Panduan Pemrograman Sistem, 8.2.3.3 Toko Tidak Diurutkan Ulang Dengan Muatan Sebelumnya.


Catatan Kaki 1: mengabaikan toko NT yang dipesan dengan lemah; inilah mengapa Anda biasanya sfencesetelah melakukan toko NT. Implementasi C11 / C ++ 11 menganggap Anda tidak menggunakan toko NT. Jika ya, gunakan _mm_sfencesebelum operasi rilis untuk memastikannya menghormati toko NT Anda. (Secara umum tidak menggunakan _mm_mfence/ _mm_sfencedalam kasus lain ; biasanya Anda hanya perlu memblokir penyusunan ulang waktu kompilasi. Atau tentu saja hanya menggunakan stdatomic.)

Peter Cordes
sumber
Saya menemukan x86-TSO: Model Programmer yang Ketat dan Dapat Digunakan untuk Multiprosesor x86 lebih mudah dibaca daripada Deskripsi Formal (terkait) yang Anda referensikan. Tetapi ambisi saya yang sebenarnya adalah untuk sepenuhnya memahami bagian 5.1.2.4 dan 7.17.3 dari Standar C11 / C18. Secara khusus, saya pikir saya mendapatkan Release / Acquire / Acquire + Release, tetapi memory_order_seq_cst didefinisikan secara terpisah dan saya berjuang untuk melihat bagaimana mereka semua cocok bersama :-(
Chris Hall
@ ChrisHall: Saya merasa terbantu dengan menyadari betapa lemahnya acq / rel, dan untuk itu Anda perlu melihat mesin seperti POWER yang dapat melakukan pengaturan ulang IRIW. (yang seq-cst melarang tetapi acq / rel tidak). Akankah dua atom menulis ke lokasi berbeda di utas berbeda selalu terlihat dalam urutan yang sama oleh utas lain? . Juga Bagaimana cara mencapai penghalang StoreLoad di C ++ 11? memiliki beberapa diskusi tentang betapa kecilnya standar yang secara formal menjamin tentang pemesanan di luar kasus-kasus yang terstruktur-dengan atau semuanya-seq-cst.
Peter Cordes
@ChrisHall: Hal utama yang dilakukan seq-cst adalah memblokir pemesanan ulang StoreLoad. (Pada x86 itulah satu-satunya yang dilakukan di luar acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act menggunakan asm, tapi ini setara dengan seq-cst vs acq / rel
Peter Cordes