Apakah penjelasan tentang pemesanan yang santai salah dalam preferensi?

13

Dalam dokumentasi std::memory_orderdi cppreference.com ada contoh pemesanan santai:

Pemesanan santai

Operasi atom yang ditandai memory_order_relaxedbukan operasi sinkronisasi; mereka tidak memaksakan urutan di antara akses memori bersamaan. Mereka hanya menjamin konsistensi urutan atomitas dan modifikasi.

Misalnya, dengan x dan y awalnya nol,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

diizinkan untuk menghasilkan r1 == r2 == 42 karena, meskipun A diurutkan-sebelum B dalam utas 1 dan C diurutkan sebelum D dalam utas 2, tidak ada yang mencegah D muncul sebelum A dalam urutan modifikasi y, dan B dari muncul sebelum C dalam urutan modifikasi x. Efek samping D pada y dapat terlihat dengan beban A di ulir 1 sedangkan efek samping B pada x dapat terlihat dengan beban C di ulir 2. Secara khusus, ini dapat terjadi jika D selesai sebelum C di utas 2, baik karena penyusunan ulang kompiler atau saat runtime.

dikatakan "C diurutkan sebelum D dalam utas 2".

Menurut definisi urutan-sebelumnya, yang dapat ditemukan dalam Urutan evaluasi , jika A diurutkan sebelum B, maka evaluasi A akan diselesaikan sebelum evaluasi B dimulai. Karena C diurutkan sebelum D dalam utas 2, C harus diselesaikan sebelum D dimulai, maka bagian kondisi dari kalimat terakhir snapshot tidak akan pernah terpuaskan.

abigaile
sumber
Apakah pertanyaan Anda khusus tentang C ++ 11?
curiousguy
tidak, itu juga berlaku untuk c ++ 14,17. Saya tahu baik kompiler dan CPU dapat menyusun ulang C dengan D. Tetapi jika penataan ulang terjadi, C tidak dapat diselesaikan sebelum D dimulai. Jadi saya pikir ada penyalahgunaan istilah dalam kalimat "A diurutkan-sebelum B dalam utas 1 dan C diurutkan sebelum D dalam utas 2". Lebih akurat untuk mengatakan "Dalam kode, A DITEMPATKAN SEBELUM B dalam thread 1 dan C DITEMPATKAN SEBELUM D dalam thread 2". Tujuan dari pertanyaan ini adalah untuk mengkonfirmasi pemikiran ini
abigail
Tidak ada yang didefinisikan dalam istilah "pemesanan ulang".
curiousguy

Jawaban:

13

Saya percaya bahwa preferensi adalah benar. Saya pikir ini bermuara pada aturan "as-if" [intro.execution] / 1 . Kompiler hanya terikat untuk mereproduksi perilaku yang dapat diamati dari program yang dijelaskan oleh kode Anda. Hubungan berurutan-sebelum hanya dibuat antara evaluasi dari perspektif utas di mana evaluasi ini dilakukan [intro.execution] / 15 . Itu berarti ketika dua evaluasi diurutkan satu demi satu muncul di suatu tempat di beberapa utas, kode yang sebenarnya berjalan di utas tersebut harus berperilaku seolah-olah apa pun yang dilakukan evaluasi pertama memang memengaruhi apa pun yang dilakukan evaluasi kedua. Sebagai contoh

int x = 0;
x = 42;
std::cout << x;

harus mencetak 42. Namun, kompiler sebenarnya tidak harus menyimpan nilai 42 ke objek xsebelum membaca nilai kembali dari objek itu untuk mencetaknya. Mungkin juga perlu diingat bahwa nilai terakhir untuk disimpan xadalah 42 dan kemudian hanya mencetak nilai 42 langsung sebelum melakukan penyimpanan sebenarnya dari nilai 42 ke x. Bahkan, jika xmerupakan variabel lokal, mungkin juga hanya melacak nilai apa variabel terakhir ditugaskan pada titik mana pun dan bahkan tidak pernah membuat objek atau benar-benar menyimpan nilai 42. Tidak ada cara bagi utas untuk mengetahui perbedaannya. Perilaku selalu akan seolah-olah ada variabel dan seolah-olah nilai 42 sebenarnya disimpan di objek x sebelumnyasedang dimuat dari objek itu. Tetapi itu tidak berarti bahwa kode mesin yang dihasilkan harus benar-benar menyimpan dan memuat apa pun di mana pun. Semua yang diperlukan adalah bahwa perilaku yang dapat diamati dari kode mesin yang dihasilkan tidak dapat dibedakan dari perilaku apa yang akan terjadi jika semua hal ini benar-benar terjadi.

Jika kita melihat

r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

maka ya, C diurutkan sebelum D. Tetapi jika dilihat dari utas ini secara terpisah, tidak ada yang C lakukan yang mempengaruhi hasil D. Dan tidak ada yang D lakukan akan mengubah hasil C. Satu-satunya cara seseorang dapat mempengaruhi yang lain adalah sebagai konsekuensi tidak langsung dari sesuatu yang terjadi di utas lainnya. Namun, dengan menentukan std::memory_order_relaxed, Anda menyatakan secara eksplisitbahwa urutan muatan dan penyimpanan diamati oleh utas lainnya tidak relevan. Karena tidak ada utas lain yang dapat mengamati beban dan penyimpanan dalam urutan tertentu, tidak ada utas lain yang dapat dilakukan untuk membuat C dan D saling memengaruhi secara konsisten. Dengan demikian, urutan di mana beban dan penyimpanan sebenarnya dilakukan tidak relevan. Dengan demikian, kompiler bebas untuk memesan ulang. Dan, seperti yang disebutkan dalam penjelasan di bawah contoh itu, jika toko dari D dilakukan sebelum memuat dari C, maka r1 == r2 == 42 memang bisa terjadi ...

Michael Kenzel
sumber
Jadi pada dasarnya standar menyatakan bahwa C harus terjadi sebelum D , tetapi kompiler percaya bahwa tidak dapat dibuktikan apakah C atau D terjadi selanjutnya dan, karena aturan as-if, mengatur ulang mereka, toh?
Fureeish
4
@Fureeish No. C harus terjadi sebelum D sejauh yang dapat diketahui oleh utasnya. Mengamati dari konteks lain mungkin tidak konsisten dengan pandangan itu.
Deduplicator
5
@curiousguy Klaim ini tampaknya mirip dengan penginjilan C ++ Anda yang lain, sebelumnya .
Lightness Races di Orbit
1
@curiousguy Michael telah memposting penjelasan panjang tentangnya bersama dengan tautan ke bab-bab yang relevan dalam standar.
Lightness Races di Orbit
2
@curiousguy Standar ini memberi label salah satu dari ketentuan itu "aturan as-if" dalam catatan kaki: "Ketentuan ini kadang-kadang disebut aturan" as-jika "" intro.execution
Caleth
1

Kadang-kadang mungkin untuk suatu tindakan dipesan relatif terhadap dua urutan tindakan lainnya, tanpa menyiratkan urutan relatif tindakan di urutan tersebut relatif terhadap satu sama lain.

Misalkan, misalnya, seseorang memiliki tiga peristiwa berikut:

  • simpan 1 hingga p1
  • memuat p2 ke dalam temp
  • simpan 2 hingga p3

dan pembacaan p2 dipesan secara independen setelah penulisan p1 dan sebelum penulisan p3, tetapi tidak ada pemesanan khusus di mana p1 dan p3 mengambil bagian. Bergantung pada apa yang dilakukan dengan p2, mungkin tidak praktis bagi kompiler untuk menunda p1 melewati p3 dan masih mencapai semantik yang diperlukan dengan p2. Misalkan, kompiler tahu bahwa kode di atas adalah bagian dari urutan yang lebih besar:

  • store 1 to p2 [diurutkan sebelum memuat p2]
  • [lakukan di atas]
  • simpan 3 ke p1 [diurutkan setelah toko lain ke p1]

Dalam hal ini, ia dapat menentukan bahwa ia dapat menyusun ulang toko ke p1 setelah kode di atas dan menggabungkannya dengan toko berikut, sehingga menghasilkan kode yang menulis p3 tanpa menulis p1 terlebih dahulu:

  • setel ke 1
  • simpan temp ke p2
  • simpan 2 hingga p3
  • simpan 3 hingga p1

Meskipun kelihatannya dependensi data akan menyebabkan bagian tertentu dari hubungan sekuensing untuk berperilaku secara transitif, kompiler dapat mengidentifikasi situasi di mana dependensi data yang nyata tidak ada, dan dengan demikian tidak akan memiliki efek transitif yang diharapkan.

supercat
sumber
1

Jika ada dua pernyataan, kompiler akan menghasilkan kode dalam urutan berurutan sehingga kode untuk yang pertama akan ditempatkan sebelum yang kedua. Tapi CPU internal memiliki pipa dan mampu menjalankan operasi perakitan secara paralel. Pernyataan C adalah instruksi muatan. Ketika memori diambil, saluran pipa akan memproses beberapa instruksi berikutnya dan mengingat mereka tidak bergantung pada instruksi pemuatan, mereka bisa berakhir dieksekusi sebelum C selesai (mis. Data untuk D ada di cache, C di memori utama).

Jika pengguna benar-benar membutuhkan dua pernyataan untuk dieksekusi secara berurutan, operasi pemesanan memori yang lebih ketat dapat digunakan. Secara umum pengguna tidak peduli asalkan program secara logis benar.

edwinc
sumber
-10

Apa pun yang Anda pikirkan sama validnya. Standar tidak mengatakan apa yang dieksekusi secara berurutan, apa yang tidak, dan bagaimana hal itu dapat digabungkan .

Terserah Anda, dan untuk setiap programmer, untuk membuat semantik yang konsisten di atas kekacauan itu, sebuah karya yang layak untuk beberapa PhD.

curiousguy
sumber