C ++ 11 memperkenalkan model memori standar. Apa artinya ini? Dan bagaimana ini akan mempengaruhi pemrograman C ++?

C ++ 11 memperkenalkan model memori standar, tetapi apa sebenarnya artinya ini? Dan bagaimana ini akan mempengaruhi pemrograman C ++?

Artikel ini ( Gavin Clark mengutip Herb Sutter ) mengatakan itu

Model memori berarti bahwa kode C ++ sekarang memiliki pustaka standar untuk pemanggilan, terlepas dari siapa yang membuat kompiler dan di platform mana ia dijalankan. Ada cara standar untuk mengontrol bagaimana utas berbeda berbicara dengan memori prosesor.

"Ketika Anda berbicara tentang memisahkan [kode] menjadi inti yang berbeda dalam standar, kami berbicara tentang model memori. Kami akan mengoptimalkannya tanpa me>Satter .

Yah, saya bisa mengingat paragraf ini dan yang serupa yang tersedia di Internet (karena saya punya model memori sendiri sejak lahir: P), dan saya bahkan bisa menulis jawaban untuk pertanyaan yang diajukan oleh orang lain, tapi jujur, saya tidak bisa Saya mengerti ini.

Pemrogram C ++ digunakan untuk mengembangkan aplikasi multi-threaded lebih awal, jadi seberapa pentingkah jika itu adalah thread POSIX atau Windows threads atau C ++ 11 threads? Apa manfaatnya? Saya ingin memahami detail level rendah.

Saya juga merasa bahwa model memori C ++ 11 terkait dengan dukungan multi-threading untuk C ++ 11, karena saya sering melihat keduanya. Jika ya, bagaimana caranya? Mengapa mereka harus berhubungan?

Karena saya tidak tahu bagaimana bekerja dengan banyak utas dan model memori mana yang secara keseluruhan, tolong bantu saya memahami konsep-konsep ini. :-)

1625
12 июня '11 в 2:30 2011-06-12 02:30 Nawaz diatur pada 12 Juni '11 pukul 2:30 pagi 2011-06-12 02:30
@ 6 balasan

Pertama, Anda harus belajar berpikir seperti pengacara berdasarkan bahasa.

Spesifikasi C ++ tidak merujuk ke kompiler, sistem operasi atau prosesor tertentu. Dia mengacu pada mesin abstrak, yang merupakan generalisasi dari sistem nyata. Dalam dunia pengacara, tugas seorang programmer adalah menulis kode untuk mesin abstrak; tugas kompiler adalah mengimplementasikan kode ini pada mesin tertentu. Jika Anda benar-benar kode spesifikasi, Anda dapat yakin bahwa kode Anda akan dikompilasi dan dijalankan tidak berubah pada sistem apa pun dengan kompiler C ++ yang kompatibel, baik hari ini atau setelah 50 tahun.

Mesin abstrak dalam spesifikasi C ++ 98 / C ++ 03 pada dasarnya adalah single-threaded. Jadi, tidak mungkin untuk menulis kode C ++ multi-threaded, yang sepenuhnya ditransfer sesuai spesifikasi. Spesifikasi ini bahkan tidak mengatakan apa-apa tentang atomicity pemuatan dan penyimpanan memori atau urutan memuat dan menyimpan data, belum lagi hal-hal seperti mutex.

Tentu saja, Anda dapat menulis kode multithreaded dalam praktiknya untuk sistem tertentu - misalnya, pthreads atau Windows. Tetapi tidak ada cara standar untuk menulis kode multi-utas untuk C ++ 98 / C ++ 03.

Mesin abstrak dalam desain multi-utas C ++ 11. Ini juga memiliki model memori yang terdefinisi dengan baik; yaitu, ia mengatakan bahwa kompiler dapat dan tidak dapat melakukan ketika datang ke akses memori.

Pertimbangkan contoh berikut ini di mana sepasang variabel global diakses secara bersamaan oleh dua utas:

  Global int x, y; Thread 1 Thread 2 x = 17; cout << y << " "; y = 37; cout << x << endl; 

Apa yang dapat dihasilkan Thread 2?

Dalam C ++ 98 / C ++ 03, ini bahkan bukan perilaku tidak terbatas; pertanyaan itu sendiri tidak ada artinya, karena standar tidak mempertimbangkan apa pun yang disebut "utas."

Dalam C ++ 11, hasilnya adalah perilaku yang tidak terdefinisi, karena muatan dan toko tidak harus atomik sama sekali. Itu mungkin tidak terlihat seperti peningkatan yang sangat baik ... Dan dalam dirinya sendiri tidak.

Tetapi dengan C ++ 11, Anda dapat menulis yang berikut ini:

  Global atomic<int> x, y; Thread 1 Thread 2 x.store(17); cout << y.load() << " "; y.store(37); cout << x.load() << endl; 

Sekarang semuanya menjadi jauh lebih menarik. Pertama-tama, perilaku didefinisikan di sini. Thread 2 sekarang dapat mencetak 0 0 (jika itu bekerja sebelum Thread 1), 37 17 (jika berjalan setelah Thread 1) atau 0 17 (jika dimulai setelah Thread 1 memberikan x, tetapi sebelum ia menetapkan y ).

Apa yang tidak dapat dicetak adalah 37 0 karena mode default untuk muatan / penyimpanan atom dalam C ++ 11 adalah untuk memastikan konsistensi yang konsisten. Ini berarti bahwa semua beban dan penyimpanan harus "seolah-olah", mereka terjadi dalam urutan di mana Anda merekamnya di setiap aliran, sementara operasi antara aliran dapat bergantian, tetapi sistem ini menyenangkan. Dengan demikian, perilaku default Atomics menyediakan atomicity dan urutan pemuatan dan penyimpanan.

Sekarang, pada prosesor modern, memastikan konsistensi yang konsisten dapat mahal. Khususnya, kompiler mungkin memancarkan hambatan memori skala penuh antara setiap akses di sini. Tetapi jika algoritme Anda dapat mentolerir muatan dan toko yang tidak dikelola; yaitu jika memerlukan atomisitas, tetapi tidak memesan; yaitu jika dia dapat mengambil 37 0 sebagai jalan keluar dari program ini, maka Anda dapat menulis ini:

  Global atomic<int> x, y; Thread 1 Thread 2 x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " "; y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl; 

Semakin modern prosesor, semakin besar kemungkinan akan lebih cepat dari contoh sebelumnya.

Akhirnya, jika Anda hanya perlu menyimpan barang dan toko tertentu, Anda dapat menulis:

  Global atomic<int> x, y; Thread 1 Thread 2 x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " "; y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl; 

Ini membawa kita kembali ke beban dan penyimpanan yang teratur - sehingga 37 0 tidak lagi merupakan jalan keluar yang mungkin, tetapi melakukannya dengan overhead yang minimal. (Dalam contoh sepele ini, hasilnya sama dengan konsistensi konsisten skala penuh; dalam program yang lebih besar ini tidak akan terjadi).

Tentu saja, jika hanya keluaran yang ingin Anda lihat, 0 0 atau 37 17 , Anda bisa membungkus mutex di sekitar kode sumber. Tetapi jika Anda membacanya jauh-jauh, saya yakin Anda sudah tahu cara kerjanya, dan jawaban ini lebih panjang dari yang saya kira :-).

Jadi intinya. Mutex itu bagus, dan C ++ 11 membuat standar. Tetapi kadang-kadang, untuk alasan kinerja, Anda memerlukan primitif tingkat rendah (misalnya, pola klasik dengan pemeriksaan kunci ganda ). Standar baru ini menyediakan gadget tingkat tinggi, seperti mutex dan variabel keadaan, dan juga menyediakan gadget tingkat rendah, seperti jenis atom dan berbagai opsi perlindungan memori. Jadi, sekarang Anda dapat menulis rutin paralel kinerja tinggi yang kompleks sepenuhnya dalam bahasa yang ditentukan oleh standar, dan Anda dapat yakin bahwa kode Anda akan dikompilasi dan berfungsi tanpa perubahan baik hari ini maupun besok.

Meskipun, jujur ​​saja, jika Anda bukan seorang ahli dan tidak bekerja pada beberapa kode tingkat rendah yang serius, Anda mungkin harus tetap berpegang pada mutex dan kondisi variabel. Inilah yang ingin saya lakukan.

Lihat posting blog ini untuk lebih jelasnya.

1877
12 июня '11 в 3:23 2011-06-12 03:23 jawabannya diberikan oleh Nemo pada 12 Juni '11 di 3:23 2011-06-12 03:23

Saya hanya akan memberikan analogi yang dengannya saya memahami model konsistensi memori (atau model memori, singkatnya). Dia terinspirasi oleh kertas semantik Lesley Lamport, "Waktu, jam, dan urutan acara dalam sistem terdistribusi . " Analogi itu relevan dan sangat penting, tetapi mungkin berlebihan bagi banyak orang. Namun, saya berharap ini memberikan gambar mental (representasi grafis), yang membuatnya lebih mudah untuk berpikir tentang model konsistensi memori.

Memungkinkan Anda untuk melihat riwayat semua lokasi memori dalam diagram ruang-waktu, di mana sumbu horizontal mewakili ruang alamat (mis., Setiap sel memori diwakili oleh titik pada sumbu ini), dan sumbu vertikal mewakili waktu (kita akan melihat bahwa secara umum , tidak ada konsep waktu universal). Dengan demikian, sejarah nilai yang disimpan di setiap sel memori diwakili oleh kolom vertikal di alamat memori ini. Setiap perubahan nilai disebabkan oleh fakta bahwa salah satu utas menulis nilai baru ke tempat ini. Di bawah gambar memori, kita akan memahami totalitas / kombinasi nilai semua lokasi memori yang diamati pada waktu tertentu , menggunakan aliran tertentu .

Kutipan dari "Pendiri Koherensi dan Konsistensi Cache"

Model memori intuitif (dan paling restriktif) adalah konsistensi sekuensial (SC), di mana eksekusi multi-ulir akan terlihat seperti pergantian eksekusi berturut-turut dari setiap utas komposit, seolah-olah utas tersebut di-multipleks waktu pada prosesor single-core.

Urutan memori global ini dapat bervariasi dari satu program yang dijalankan ke yang lain dan mungkin tidak diketahui sebelumnya. Fitur karakteristik SC adalah serangkaian irisan horizontal dalam diagram ruang-waktu yang mewakili bidang simultanitas (mis. Gambar dalam memori). Di pesawat ini, semua acara (atau nilai memori) simultan. Ada konsep waktu absolut di mana semua utas setuju dengan nilai memori yang simultan. Di SC, hanya ada satu gambar memori pada satu waktu, yang umum untuk semua utas. Artinya, pada setiap saat dalam waktu semua prosesor konsisten dengan gambar memori (mis. Konten memori agregat). Ini tidak hanya berarti bahwa semua utas melihat urutan nilai yang sama untuk semua lokasi memori, tetapi juga bahwa semua prosesor menjalankan kombinasi nilai yang sama untuk semua variabel. Ini sama dengan mengatakan bahwa semua operasi memori (dalam semua sel memori) diamati dalam urutan penuh yang sama oleh semua utas.

Pada model dengan memori yang melemah, setiap utas akan memisahkan ruang-waktu-alamat dengan caranya sendiri, satu-satunya batasan adalah bahwa potongan masing-masing aliran tidak bersinggungan satu sama lain, karena semua utas harus cocok dengan sejarah setiap sel memori individu (tentu saja, keping utas berbeda-beda) mungkin dan akan bersinggungan satu sama lain). Tidak ada cara universal untuk memotongnya (tanpa dedaunan khusus dari ruang-waktu). Irisan tidak boleh rata (atau linier). Mereka dapat melengkung, dan inilah yang dapat dilakukan oleh nilai-nilai aliran yang ditulis oleh aliran lain, dari yang di mana mereka ditulis. Cerita-cerita dari berbagai tempat memori dapat bergeser (atau meregang) secara relatif terhadap satu sama lain saat melihat aliran tertentu . Setiap utas akan memiliki gagasan yang berbeda tentang peristiwa mana (atau, yang setara, nilai memori) secara bersamaan. Set peristiwa (atau nilai memori) yang secara bersamaan dikaitkan dengan satu aliran tidak bersamaan dengan yang lainnya. Jadi, dalam model memori yang lemah, semua utas masih mempertahankan histori yang sama (mis., Urutan nilai) untuk setiap lokasi memori. Tetapi mereka dapat mengamati gambar memori yang berbeda (mis. Kombinasi nilai semua lokasi memori). Bahkan jika dua lokasi memori yang berbeda direkam oleh aliran yang sama secara berurutan, dua nilai yang baru direkam dapat diamati dalam urutan yang berbeda oleh aliran lain.

[Ilustrasi Wikipedia]

Pembaca yang akrab dengan Relativitas Khusus Einstein akan memperhatikan apa yang saya bicarakan. Terjemahan kata-kata Minkowski ke dalam bidang model memori: ruang alamat dan waktu adalah bayangan ruang-waktu alamat. Dalam hal ini, setiap pengamat (mis., Flow) akan memproyeksikan bayangan peristiwa (mis., Menghafal memori / beban) ke garis dunianya sendiri (yaitu sumbu waktunya) dan bidang simultannya sendiri (poros ruang alamatnya) ) Tema dalam model memori C ++ 11 sesuai dengan pengamat yang bergerak relatif satu sama lain dalam teori relativitas khusus. Konsistensi yang konsisten sesuai dengan ruang-waktu Galilea (mis. Semua pengamat sepakat pada satu urutan peristiwa absolut dan rasa simultanitas global).

Kesamaan antara model memori dan teori relativitas khusus berasal dari fakta bahwa keduanya mendefinisikan serangkaian peristiwa yang dipesan sebagian, sering disebut set kausal. Beberapa peristiwa (mis., Penyimpanan) dapat memengaruhi (tetapi tidak memengaruhi) acara lainnya. Aliran C ++ 11 (atau pengamat dalam fisika) tidak lebih dari sebuah rantai (yaitu, rangkaian yang terurut sepenuhnya) dari peristiwa (misalnya, memori dimuat dan disimpan ke kemungkinan alamat yang berbeda).

Dalam teori relativitas, beberapa tatanan dikembalikan ke gambaran yang tampaknya kacau tentang peristiwa yang dipesan sebagian, karena satu-satunya tatanan waktu yang disetujui oleh semua pengamat adalah memesan di antara "peristiwa sementara" (yaitu, peristiwa yang pada prinsipnya dapat dihubungkan oleh partikel apa pun). lebih lambat dari kecepatan cahaya dalam ruang hampa). Hanya acara yang diatur waktunya yang selalu dipesan. Waktu dalam Fisika, Craig Callender .

Dalam model memori C ++ 11, mekanisme yang sama (model konsistensi-rilis-rilis) digunakan untuk membangun hubungan sebab akibat lokal ini.

Untuk memastikan penentuan urutan memori dan motivasi penolakan dari SC, saya akan memberikan dari Primer tentang konsistensi memori dan konsistensi cache.

Untuk komputer memori bersama, model konsistensi memori menentukan perilaku yang terlihat secara arsitektur dari sistem memorinya. Kriteria untuk kebenaran inti prosesor tunggal membagi perilaku antara "satu hasil yang benar" dan "banyak alternatif tidak teratur." Hal ini disebabkan oleh fakta bahwa arsitektur prosesor menyatakan bahwa eksekusi thread mengubah status input yang ditentukan menjadi satu kondisi output yang terdefinisi dengan baik, bahkan pada core out of order. Namun, model konsistensi dengan memori bersama berhubungan dengan pemuatan dan penyimpanan banyak utas dan biasanya memungkinkan banyak eksekusi yang benar, menghindari banyak (lebih) yang salah. Kemungkinan beberapa eksekusi yang benar adalah karena fakta bahwa ISA memungkinkan eksekusi simultan dari beberapa utas, seringkali dengan banyak kemungkinan intersepsi perintah yang sah dari utas yang berbeda.

Model konsistensi memori yang santai atau lemah dimotivasi oleh kenyataan bahwa sebagian besar pemesanan memori dalam model yang kuat tidak diperlukan. Jika aliran memperbarui sepuluh elemen data dan kemudian bendera sinkronisasi, biasanya tidak penting bagi programmer apakah elemen data diperbarui secara berurutan satu sama lain, dan hanya semua elemen data yang diperbarui sebelum bendera diperbarui (mereka biasanya diimplementasikan menggunakan instruksi FENCE). Model yang santai cenderung menangkap fleksibilitas pesanan yang meningkat ini dan hanya mempertahankan pesanan yang "dibutuhkan" oleh programmer untuk mendapatkan kinerja yang lebih baik dan akurasi SC. Misalnya, dalam beberapa arsitektur, buffer tulis FIFO digunakan oleh setiap kernel untuk menyimpan hasil penyimpanan tetap (jarak jauh) sebelum menulis hasilnya ke cache. Optimalisasi ini meningkatkan kinerja, tetapi me>

Menyusun u>

Karena konsistensi cache dan konsistensi memori kadang-kadang membingungkan, itu juga instruktif untuk memiliki kutipan ini:

В отличие от согласованности, когерентность кэша не отображается ни в программном обеспечении, ни в запросе. Когерентность направлена ​​на то, чтобы кэши системы с разделяемой памятью были функционально невидимы как кеши в одноядерной системе. Правильная согласованность гарантирует, что программист не может определить, имеет ли и где система кэширует, анализируя результаты нагрузок и хранилищ. Это связано с тем, что правильная когерентность гарантирует, что кэши никогда не будут включать новое или другое поведение функционировать (программисты могут все еще иметь возможность вывести вероятную структуру кэша, используя информацию время ). Основная цель протоколов когерентности кеша - поддерживать инвариант одиночного писателя-множественного считывателя (SWMR) для каждой ячейки памяти. Важным различием между согласованностью и согласованностью является то, что согласованность указана в на основе расположения памяти , тогда как согласованность указана в отношении местоположений памяти all .