Apakah "volatile" menjamin semuanya dalam kode C portabel untuk sistem multi-core?

12

Setelah melihat sekelompok dari lainnya pertanyaan dan mereka jawaban , saya mendapatkan kesan bahwa tidak ada kesepakatan luas tentang apa yang "volatile" kata kunci dalam C berarti persis.

Bahkan standar itu sendiri tampaknya tidak cukup jelas bagi semua orang untuk menyetujui apa artinya .

Di antara masalah lain:

  1. Tampaknya memberikan jaminan yang berbeda tergantung pada perangkat keras Anda dan tergantung pada kompiler Anda.
  2. Ini mempengaruhi pengoptimalan kompiler tetapi bukan pengoptimalan perangkat keras, jadi pada prosesor tingkat lanjut yang melakukan pengoptimalan run-time sendiri, bahkan tidak jelas apakah kompiler dapat mencegah pengoptimalan apa pun yang ingin Anda cegah. (Beberapa kompiler memang menghasilkan instruksi untuk mencegah beberapa optimasi perangkat keras pada beberapa sistem, tetapi ini tampaknya tidak distandarisasi dengan cara apa pun.)

Untuk meringkas masalah, tampak (setelah membaca banyak) bahwa "volatile" menjamin sesuatu seperti: Nilai akan dibaca / ditulis tidak hanya dari / ke register, tetapi setidaknya ke cache L1 inti, dalam urutan yang sama yang baca / tulis muncul dalam kode. Tapi ini tampaknya tidak berguna, karena membaca / menulis dari / ke register sudah cukup dalam utas yang sama, sementara berkoordinasi dengan L1 cache tidak menjamin apa pun lebih lanjut mengenai koordinasi dengan utas lainnya. Saya tidak bisa membayangkan kapan bisa penting untuk melakukan sinkronisasi hanya dengan cache L1.

PENGGUNAAN 1.
Satu-satunya penggunaan volatile yang disepakati secara luas tampaknya untuk sistem lama atau tertanam di mana lokasi memori tertentu dipetakan perangkat keras ke fungsi I / O, seperti sedikit dalam memori yang mengontrol (langsung, dalam perangkat keras) lampu. , atau sedikit dalam memori yang memberi tahu Anda apakah tombol keyboard turun atau tidak (karena terhubung oleh perangkat keras langsung ke tombol).

Tampaknya "gunakan 1" tidak terjadi dalam kode portabel yang targetnya mencakup sistem multi-core.

PENGGUNAAN 2
Tidak jauh berbeda dari "penggunaan 1" adalah memori yang dapat dibaca atau ditulis kapan saja oleh penangan interupsi (yang mungkin mengontrol lampu atau menyimpan info dari kunci). Tetapi sudah untuk ini kita memiliki masalah yang tergantung pada sistem, penangan interrupt mungkin berjalan pada inti yang berbeda dengan cache memori sendiri , dan "volatile" tidak menjamin koherensi cache pada semua sistem.

Jadi "use 2" tampaknya melampaui apa yang "volatile" dapat berikan.

GUNAKAN 3
Satu-satunya penggunaan tak terbantahkan lainnya yang saya lihat adalah untuk mencegah kesalahan optimasi akses melalui variabel yang berbeda yang menunjuk ke memori yang sama yang tidak disadari oleh kompiler adalah memori yang sama. Tetapi ini mungkin hanya tidak perlu dipermasalahkan karena orang tidak membicarakannya - saya hanya melihat satu menyebutkannya. Dan saya pikir standar C sudah mengakui bahwa pointer "berbeda" (seperti argumen yang berbeda untuk suatu fungsi) mungkin menunjuk ke item yang sama atau item terdekat, dan sudah menentukan bahwa kompiler harus menghasilkan kode yang berfungsi bahkan dalam kasus seperti itu. Namun, saya tidak dapat dengan cepat menemukan topik ini dalam standar (500 halaman!) Terbaru.

Jadi "gunakan 3" mungkin tidak ada sama sekali?

Karena itu pertanyaan saya:

Apakah "volatile" menjamin semuanya dalam kode C portabel untuk sistem multi-core?


EDIT - perbarui

Setelah browsing standar terbaru , sepertinya jawabannya paling tidak ya sangat terbatas:
1. Standar berulang kali menentukan perlakuan khusus untuk tipe spesifik "volatile sig_atomic_t". Namun standar juga mengatakan bahwa penggunaan fungsi sinyal dalam program multi-threaded menghasilkan perilaku yang tidak terdefinisi. Jadi use case ini tampaknya terbatas pada komunikasi antara program single-threaded dan pengendali sinyal.
2. Standar juga menentukan arti yang jelas untuk "volatile" dalam kaitannya dengan setjmp / longjmp. (Kode contoh yang penting diberikan dalam pertanyaan dan jawaban lain .)

Jadi pertanyaan yang lebih tepat adalah:
Apakah "volatile" menjamin semuanya dalam kode C portabel untuk sistem multi-inti, selain dari (1) memungkinkan program berulir tunggal untuk menerima informasi dari pengendali sinyal, atau (2) memungkinkan setjmp kode untuk melihat variabel yang dimodifikasi antara setjmp dan longjmp?

Ini masih merupakan pertanyaan ya / tidak.

Jika "ya", alangkah baiknya jika Anda dapat menunjukkan contoh kode portabel bebas bug yang menjadi bermasalah jika "tidak stabil" dihilangkan. Jika "tidak", maka saya kira kompiler bebas untuk mengabaikan "volatile" di luar dua kasus yang sangat spesifik ini, untuk target multi-core.

Mat
sumber
3
Sinyal ada di portable C; bagaimana dengan variabel global yang diperbarui oleh penangan sinyal? Ini perlu untuk volatilemenginformasikan program bahwa itu dapat berubah secara tidak sinkron.
Nate Eldredge
2
@NateEldredge Global, sementara hanya volatile, tidak cukup baik. Perlu atom juga.
Eugene Sh.
@EugeneSh .: Ya, tentu saja. Tetapi pertanyaan yang ada adalah tentang volatilespesifik, yang saya percaya perlu.
Nate Eldredge
" Sementara berkoordinasi dengan L1 cache tidak menjamin apa pun lebih lanjut mengenai koordinasi dengan utas lain " Di mana "berkoordinasi dengan L1 cache" tidak cukup untuk berkomunikasi dengan utas lain?
curiousguy
1
Mungkin relevan, proposal C ++ untuk menghilangkan volatile , proposal membahas banyak masalah yang Anda ajukan di sini, dan mungkin hasilnya akan berpengaruh pada komite C
MM

Jawaban:

1

Untuk meringkas masalah, tampak (setelah membaca banyak) bahwa "volatile" menjamin sesuatu seperti: Nilai akan dibaca / ditulis tidak hanya dari / ke register, tetapi setidaknya ke cache L1 inti, dalam urutan yang sama yang baca / tulis muncul dalam kode .

Tidak, sama sekali tidak . Dan itu membuat volatile hampir tidak berguna untuk tujuan kode aman MT.

Jika ya, maka volatile akan cukup baik untuk variabel yang dibagikan oleh banyak utas karena memesan peristiwa dalam cache L1 adalah semua yang perlu Anda lakukan dalam CPU biasa (baik multi-core atau multi-CPU pada motherboard) yang mampu bekerja sama dengan cara yang memungkinkan implementasi normal dari C / C ++ atau Java multithreading mungkin dengan biaya yang diharapkan biasa (yaitu, bukan biaya besar pada sebagian besar operasi mutasi atom atau tidak puas).

Tetapi volatile tidak memberikan pemesanan yang dijamin (atau "visibilitas memori") dalam cache baik secara teori maupun dalam praktik.

(Catatan: berikut ini didasarkan pada interpretasi yang kuat dari dokumen standar, maksud standar, praktik sejarah, dan pemahaman mendalam tentang harapan penulis kompiler. Pendekatan ini didasarkan pada sejarah, praktik aktual, dan harapan dan pemahaman orang nyata di dunia nyata, yang jauh lebih kuat dan lebih dapat diandalkan daripada parsing kata-kata dokumen yang tidak dikenal sebagai penulisan spesifikasi bintang dan yang telah direvisi berkali-kali.)

Dalam praktiknya, volatile memang menjamin kemampuan ptrace yaitu kemampuan untuk menggunakan informasi debug untuk program yang sedang berjalan, pada tingkat optimasi apa pun , dan fakta bahwa informasi debug masuk akal untuk objek volatil ini:

  • Anda dapat menggunakan ptrace(mekanisme seperti ptrace) untuk mengatur titik break yang berarti pada titik-titik urutan setelah operasi yang melibatkan objek volatil: Anda benar-benar dapat menembus tepat pada titik-titik ini (perhatikan bahwa ini hanya berfungsi jika Anda bersedia untuk menetapkan banyak titik break seperti halnya Pernyataan C / C ++ dapat dikompilasi ke banyak titik awal dan akhir perakitan yang berbeda, seperti dalam loop yang tidak dibuka secara masif);
  • sementara untaian eksekusi berhenti, Anda dapat membaca nilai semua objek volatil, karena mereka memiliki representasi kanonik mereka (mengikuti ABI untuk jenisnya masing-masing); variabel lokal yang tidak mudah berubah dapat memiliki representasi atipikal, f.ex. representasi bergeser: variabel yang digunakan untuk mengindeks array mungkin dikalikan dengan ukuran objek individu, untuk memudahkan pengindeksan; atau mungkin diganti oleh pointer ke elemen array (selama semua penggunaan variabel dikonversi sama) (pikirkan mengubah dx menjadi du dalam integral);
  • Anda juga dapat memodifikasi objek-objek tersebut (selama pemetaan memori mengizinkannya, karena objek yang mudah menguap dengan masa pakai statis yang memenuhi syarat mungkin berada dalam rentang memori yang dipetakan hanya baca).

Jaminan yang mudah menguap dalam praktiknya sedikit lebih dari interpretasi ptrace yang ketat: ia juga menjamin bahwa variabel otomatis yang tidak stabil memiliki alamat pada stack, karena mereka tidak dialokasikan untuk register, alokasi register yang akan membuat manipulasi ptrace lebih halus (kompiler dapat output informasi debug untuk menjelaskan bagaimana variabel dialokasikan ke register, tetapi membaca dan mengubah status register sedikit lebih terlibat daripada mengakses alamat memori).

Perhatikan bahwa kemampuan debug penuh program, yang mempertimbangkan semua variabel volatile setidaknya pada titik-titik urutan, disediakan oleh mode "optimisasi nol" dari kompiler, mode yang masih melakukan optimasi sepele seperti penyederhanaan aritmatika (biasanya tidak ada jaminan tidak ada optimasi di semua mode). Tetapi volatile lebih kuat daripada non optimasi: x-xdapat disederhanakan untuk integer non volatile xtetapi tidak untuk objek volatile.

Jadi volatile berarti dijamin akan dikompilasi sebagaimana adanya , seperti terjemahan dari sumber ke biner / rakitan oleh kompiler panggilan sistem bukanlah reinterpretasi, diubah, atau dioptimalkan dengan cara apa pun oleh kompiler. Perhatikan bahwa panggilan perpustakaan mungkin atau mungkin bukan panggilan sistem. Banyak fungsi sistem resmi sebenarnya adalah fungsi pustaka yang menawarkan lapisan interposisi yang tipis dan umumnya tunduk pada kernel pada akhirnya. (Khususnya getpidtidak perlu pergi ke kernel dan bisa membaca lokasi memori yang disediakan oleh OS yang berisi informasi tersebut.)

Interaksi yang mudah menguap adalah interaksi dengan dunia luar dari mesin nyata , yang harus mengikuti "mesin abstrak". Mereka bukan interaksi internal bagian-bagian program dengan bagian-bagian program lainnya. Compiler hanya dapat memberikan alasan tentang apa yang diketahuinya, yaitu bagian-bagian program internal.

Pembuatan kode untuk akses yang mudah menguap harus mengikuti interaksi paling alami dengan lokasi memori itu: harus tidak mengejutkan. Itu berarti bahwa beberapa akses yang mudah menguap diharapkan menjadi atom : jika cara alami untuk membaca atau menulis representasi dari longpada arsitektur adalah atom, maka diharapkan membaca atau menulis volatile longakan berupa atom, karena kompiler seharusnya tidak menghasilkan kode tidak efisien konyol untuk mengakses objek volatile byte demi byte, misalnya .

Anda harus dapat menentukan itu dengan mengetahui arsitekturnya. Anda tidak perlu tahu apa-apa tentang kompiler, karena volatile berarti bahwa kompiler harus transparan .

Tetapi volatile tidak lebih dari memaksa emisi perakitan yang diharapkan untuk kasus yang paling tidak dioptimalkan untuk kasus-kasus tertentu untuk melakukan operasi memori: semantik volatile berarti semantik kasus umum.

Kasus umum adalah apa yang dilakukan kompiler ketika tidak memiliki informasi tentang konstruk: f.ex. memanggil fungsi virtual pada nilai melalui pengiriman dinamis adalah kasus umum, membuat panggilan langsung ke overrider setelah menentukan pada waktu kompilasi jenis objek yang ditunjuk oleh ekspresi adalah kasus tertentu. Kompiler selalu memiliki penanganan kasus umum dari semua konstruksi, dan mengikuti ABI.

Volatile tidak melakukan hal khusus untuk menyinkronkan utas atau menyediakan "visibilitas memori": volatile hanya memberikan jaminan pada tingkat abstrak yang terlihat dari dalam sebuah thread yang mengeksekusi atau menghentikan, yaitu bagian dalam inti CPU :

  • volatile tidak mengatakan apa pun tentang operasi memori mana yang mencapai RAM utama (Anda dapat menetapkan tipe cache memori tertentu dengan instruksi perakitan atau panggilan sistem untuk mendapatkan jaminan ini);
  • volatile tidak memberikan jaminan kapan operasi memori akan dilakukan untuk semua level cache (bahkan L1) .

Hanya poin kedua yang berarti volatile tidak berguna di sebagian besar masalah komunikasi antar utas; poin pertama pada dasarnya tidak relevan dalam masalah pemrograman yang tidak melibatkan komunikasi dengan komponen perangkat keras di luar CPU (s) tetapi masih di bus memori.

Properti volatile yang menyediakan perilaku terjamin dari sudut pandang inti yang menjalankan utas berarti bahwa sinyal asinkron dikirim ke utas itu, yang dijalankan dari sudut pandang urutan pelaksanaan utas tersebut, lihat operasi dalam urutan kode sumber .

Kecuali jika Anda berencana untuk mengirim sinyal ke utas Anda (suatu pendekatan yang sangat berguna untuk konsolidasi informasi tentang utas yang sedang berjalan tanpa titik penghentian yang disepakati sebelumnya), volatil bukan untuk Anda.

curiousguy
sumber
6

Saya bukan ahli, tetapi cppreference.com memberi saya informasi yangvolatile cukup bagus . Inilah intinya:

Setiap akses (baik baca dan tulis) yang dibuat melalui ekspresi lvalue tipe volatile-kualifikasi dianggap sebagai efek samping yang dapat diamati untuk tujuan optimasi dan dievaluasi secara ketat sesuai dengan aturan mesin abstrak (yaitu, semua penulisan diselesaikan pada beberapa waktu sebelum titik urutan berikutnya). Ini berarti bahwa dalam satu utas eksekusi, akses volatil tidak dapat dioptimalkan atau disusun ulang relatif terhadap efek samping lain yang terlihat yang dipisahkan oleh titik urutan dari akses volatil.

Ini juga memberikan beberapa kegunaan:

Penggunaan volatile

1) objek volatil statis memodelkan port I / O yang dipetakan memori, dan objek volatile const statis memodelkan port input dipetakan memori, seperti jam waktu nyata

2) objek volatile statis tipe sig_atomic_t digunakan untuk komunikasi dengan penangan sinyal.

3) variabel volatil yang bersifat lokal ke fungsi yang berisi doa setjmp makro adalah satu-satunya variabel lokal yang dijamin untuk mempertahankan nilainya setelah pengembalian longjmp.

4) Selain itu, variabel volatil dapat digunakan untuk menonaktifkan bentuk optimasi tertentu, misalnya untuk menonaktifkan penghapusan toko mati atau lipat konstan untuk microbenchmark.

Dan tentu saja, itu menyebutkan yang volatiletidak berguna untuk sinkronisasi utas:

Perhatikan bahwa variabel volatil tidak cocok untuk komunikasi antar utas; mereka tidak menawarkan atomicity, sinkronisasi, atau pemesanan memori. Pembacaan dari variabel volatil yang dimodifikasi oleh utas lain tanpa sinkronisasi atau modifikasi bersamaan dari dua utas yang tidak disinkronkan adalah perilaku yang tidak terdefinisi karena perlombaan data.

Fred Larson
sumber
2
Secara khusus, (2) dan (3) relevan dengan kode portabel.
Nate Eldredge
2
@ TED Terlepas dari nama domainnya, tautannya adalah informasi tentang C, bukan C ++
David Brown
@NateEldredge Anda jarang dapat menggunakan longjmpkode C ++.
curiousguy
@ David C dan C ++ memiliki definisi yang sama dari SE yang dapat diamati, dan pada dasarnya thread primitif yang sama.
curiousguy
4

Pertama-tama, secara historis ada banyak cegukan sehubungan dengan beragam interpretasi makna volatileakses dan sejenisnya. Lihat studi ini: Volatile Disalahgunakan, dan Apa yang Harus Dilakukan tentang Itu .

Terlepas dari berbagai masalah yang disebutkan dalam penelitian itu, perilaku volatileportabel, simpan untuk satu aspek dari mereka: ketika mereka bertindak sebagai hambatan memori . Penghalang memori adalah beberapa mekanisme yang ada untuk mencegah eksekusi kode Anda yang tidak dilakukan secara bersamaan. Menggunakan volatilesebagai penghalang memori tentu tidak portabel.

Apakah bahasa C menjamin perilaku memori atau tidak dari volatileitu tampaknya bisa diperdebatkan, meskipun secara pribadi saya pikir bahasa itu jelas. Pertama kita memiliki definisi formal tentang efek samping, C17 5.1.2.3:

Mengakses volatileobjek, memodifikasi objek, memodifikasi file, atau memanggil fungsi yang melakukan salah satu dari operasi tersebut adalah semua efek samping , yang merupakan perubahan dalam kondisi lingkungan eksekusi.

Standar mendefinisikan urutan sekuensing, sebagai cara menentukan urutan evaluasi (eksekusi). Definisi ini formal dan rumit:

Diurutkan sebelumnya adalah hubungan asimetris, transitif, pasangan-bijaksana antara evaluasi yang dilakukan oleh satu utas, yang menginduksi urutan parsial di antara evaluasi tersebut. Dengan adanya dua evaluasi A dan B, jika A diurutkan sebelum B, maka eksekusi A harus mendahului eksekusi B. (Sebaliknya, jika A diurutkan sebelum B, maka B diurutkan setelah A.) Jika A tidak diurutkan sebelum atau setelah B, maka A dan B tidak dilakukan . Evaluasi A dan B secara tidak pasti diurutkan ketika A diurutkan baik sebelum atau setelah B, tetapi tidak ditentukan yang mana.13) Adanya titik sekuens antara evaluasi ekspresi A dan B menyiratkan bahwa setiap perhitungan nilai dan efek samping yang terkait dengan A diurutkan sebelum setiap perhitungan nilai dan efek samping yang terkait dengan B. (Ringkasan titik-titik urutan diberikan dalam lampiran C.)

TL; DR di atas pada dasarnya adalah jika kita memiliki ekspresi Ayang mengandung efek samping, itu harus dilakukan mengeksekusi sebelum ekspresi lain B, dalam kasus Bdiurutkan setelahnya A.

Optimalisasi kode C dimungkinkan melalui bagian ini:

Dalam mesin abstrak, semua ekspresi dievaluasi sebagaimana ditentukan oleh semantik. Implementasi aktual tidak perlu mengevaluasi bagian dari ekspresi jika dapat menyimpulkan bahwa nilainya tidak digunakan dan tidak ada efek samping yang diperlukan dihasilkan (termasuk yang disebabkan oleh pemanggilan fungsi atau mengakses objek yang mudah menguap).

Ini berarti bahwa program dapat mengevaluasi (mengeksekusi) ekspresi dalam urutan yang standar mandat di tempat lain (urutan evaluasi, dll). Tetapi itu tidak perlu mengevaluasi (mengeksekusi) suatu nilai jika dapat menyimpulkan bahwa itu tidak digunakan. Misalnya, operasi 0 * xtidak perlu mengevaluasi xdan hanya mengganti ekspresi dengan 0.

Kecuali mengakses variabel adalah efek samping. Artinya dalam hal xini volatile, ia harus mengevaluasi (mengeksekusi) 0 * xmeskipun hasilnya akan selalu 0. Optimasi tidak diperbolehkan.

Selain itu, standar tersebut berbicara tentang perilaku yang dapat diamati:

Persyaratan paling tidak pada implementasi yang sesuai adalah:

  • Akses ke objek volatil dievaluasi secara ketat sesuai dengan aturan mesin abstrak.
    / - / Ini adalah perilaku yang dapat diamati dari program.

Mengingat semua hal di atas, implementasi yang sesuai (compiler + sistem yang mendasarinya) tidak dapat mengeksekusi akses volatileobjek dalam urutan yang tidak ditentukan, jika semantik dari sumber C tertulis mengatakan sebaliknya.

Ini berarti bahwa dalam contoh ini

volatile int x;
volatile int y;
z = x;
z = y;

Kedua ekspresi penugasan harus dievaluasi dan z = x; harus dievaluasi sebelumnya z = y;. Implementasi multi-prosesor yang meng-outsource dua operasi ini ke dua core unecessence yang berbeda tidak sesuai!

Dilema adalah bahwa kompiler tidak dapat berbuat banyak tentang hal-hal seperti pra-pengambilan caching dan instruksi pipelining dll, terutama tidak ketika berjalan di atas OS. Jadi kompiler menyerahkan masalah itu kepada programmer, memberi tahu mereka bahwa hambatan memori sekarang menjadi tanggung jawab programmer. Sementara standar C dengan jelas menyatakan bahwa masalah perlu diselesaikan oleh kompiler.

Kompiler tidak perlu peduli untuk menyelesaikan masalah, dan karenanya volatilebertindak sebagai penghalang memori adalah non-portabel. Ini telah menjadi masalah kualitas implementasi.

Lundin
sumber
@curiousguy Tidak masalah.
Lundin
@curiousguy Tidak masalah, asalkan itu semacam tipe integer dengan atau tanpa kualifikasi.
Lundin
Jika bilangan bulat sederhana tidak mudah menguap, mengapa penulisan yang berlebihan zharus benar-benar dieksekusi? (seperti z = x; z = y;) Nilainya akan dihapus dalam pernyataan berikutnya.
curiousguy
@curiousguy Karena membaca ke variabel volatil harus dieksekusi tidak peduli, dalam urutan yang ditentukan.
Lundin
Lalu apakah zbenar - benar ditugaskan dua kali? Bagaimana Anda tahu bahwa "membaca dieksekusi"?
curiousguy