6 Soru: C ++ 11 standart bir bellek modeli tanıttı. Bunun anlamı ne? Ve C ++ programlamayı nasıl etkileyecek?
C ++ 11 standart bir bellek modeli getirdi, fakat bu tam olarak ne anlama geliyor? Ve C ++ programlamayı nasıl etkileyecek?
Bellek modeli, C ++ kodu anlamına gelir. şimdi aramak için standart bir kütüphane var derleyiciyi kim yapmış olursa olsun ve hangi platformda çalıştığını. Kontrol etmenin standart bir yolu var. farklı konu İşlemcinin hafızası.
"Bölmekten söz ederken [code] farklı çekirdeklerde standartta hakkında konuşuyoruz Hafıza modeli. Gidiyoruz kırmadan optimize etmek varsayımların ardından insanlar gidiyor Kodu girmek için, " Sutter " dedi.
İnternette mevcut olan bu ve benzer paragrafları ezberleyebilirim (doğumdan beri kendi bellek modelime sahip olduğum gibi: P) ve hatta başkaları tarafından sorulan sorulara cevap olarak gönderebilir, ancak dürüst olmak gerekirse, bunu tam olarak anlamıyorum.
C ++ programcıları daha önce çok parçacıklı uygulamalar geliştirmek için kullanıldı, peki POSIX veya Windows veya C ++ 11 konuları nasıl olur? Faydaları nelerdir? Alt düzey detayları anlamak istiyorum.
Ayrıca, C ++ 11 bellek modelinin bir şekilde C ++ 11 çoklu iş parçacığı desteği ile ilgili olduğunu da hissediyorum. Öyleyse, tam olarak nasıl? Neden ilişkili olmalılar?
Çoklu iş parçacığı içindekilerin nasıl çalıştığını ve genel olarak bellek modelinin ne anlama geldiğini bilmediğim için, lütfen bu kavramları anlamama yardımcı olun. : -)
İlk önce, bir Dil Avukatı gibi düşünmeyi öğrenmelisiniz.
C ++ özelliği herhangi bir derleyiciye, işletim sistemine veya CPU'ya referans vermez. Gerçek sistemlerin genelleştirilmesi olan bir soyut makineye atıfta bulunur. Dil Avukatı dünyasında, programcının işi soyut makineye kod yazmaktır; derleyicinin görevi bu kodu bir beton makinesinde gerçekleştirmektir. Spesifikasyona katı bir şekilde kod yazarak, bugün veya 50 yıl sonra, uyumlu bir C ++ derleyicisine sahip herhangi bir sistemde kodunuzun derlenip çalıştırılmayacağından emin olabilirsiniz.
C ++ 98 /C ++ 03 şartnamesindeki soyut makine, temel olarak tek iş parçacıklıdır. Bu nedenle, şartnameye göre "tamamen taşınabilir" olan çok dişli C ++ kodunu yazmak mümkün değildir. Teknik özellik, bellek yüklerinin ve depoların atomikliği veya yüklerin ve depoların olabileceği siparişi hakkında hiçbir şey söylemiyor, muteksiler gibi şeyleri boşver.
Elbette, pthreads veya Windows gibi belirli somut sistemler için pratikte çok dişli kod yazabilirsiniz. Ancak C ++ 98 /C ++ 03 için çok iş parçacıklı kod yazmanın standart yolu yoktur.
C ++ 11'deki soyut makine, tasarım tarafından çok iş parçacıklı. Ayrıca iyi tanımlanmış bir bellek modeline sahiptir; yani, derleyicinin belleğe erişme konusunda yapabileceklerini ve yapamayabileceklerini söylüyor.
Bir çift genel değişkene aynı anda iki iş parçacığıyla erişilen aşağıdaki örneği göz önünde bulundurun:
Global int x, y; Thread 1 Thread 2 x = 17; cout << y << " "; y = 37; cout << x << endl;
İplik 2 çıktısı ne olabilir?
C ++ 98 /C ++ 03 altında, bu tanımsız Davranış bile değildir; Sorunun kendisi anlamsızdır çünkü standart bir "thread" olarak adlandırılan hiçbir şeyi düşünmez.
C ++ 11 altında, sonuç Tanımsız Davranış, çünkü yükler ve depoların genel olarak atomik olması gerekmez. Bu da pek bir gelişme göstermeyebilir ... Ve kendi başına, öyle değil.
Ancak C ++ 11 ile şunu yazabilirsiniz:
Global atomic<int> x, y; Thread 1 Thread 2 x.store(17); cout << y.load() << " "; y.store(37); cout << x.load() << endl;
Artık işler daha da ilginçleşiyor. Her şeyden önce, buradaki davranış tanımlıdır . İş parçacığı 2 şimdi
0 0 (iş parçacığı 1'den önce çalışıyorsa),
37 17 (iş parçacığı 1 sonra çalıştırıyorsa) veya
0 17 (iş parçacığı 1 x'e atanır ancak y için atanmadan önce çalışıyorsa) yazdırabilir.
Yazdırılamadığı şey
37 0, çünkü C ++ 11'deki atom yükleri /depoları için varsayılan mod, sıralı tutarlılığı zorlamaktır. Bu sadece tüm yüklerin ve mağazaların t'de olduğu gibi "sanki" olması gerektiği anlamına gelirBunları her bir dizinin içine yazdığını söylerken, sistemin sevdiği halde diziler arasındaki işlemler birleştirilebilir. Bu nedenle, atomiklerin varsayılan davranışı, yükler ve depolar için hem atomiklik hem de sıralama 'ı sağlar.
Artık modern bir CPU'da sıralı tutarlılığı sağlamak pahalı olabilir. Özellikle, derleyicinin burada her erişim arasında tam gelişmiş bellek engelleri yayması muhtemeldir. Fakat eğer algoritmanız sıra dışı yükleri ve depoları tolere edebilirse; yani, atomiklik gerektiriyorsa ancak sipariş vermiyorsa; yani, bu programdan elde edilen çıktı olarak
37 0’a tahammül edebiliyorsa, şunu yazabilirsiniz:
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;
İşlemci ne kadar modernse, bunun bir önceki örnekten daha hızlı olması muhtemeldir.
Son olarak, belirli yükleri ve depoları sırayla tutmanız gerekirse, şunu yazabilirsiniz:
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;
Bu, bizi sipariş edilen yüklere ve depolara geri götürür - yani
37 0 artık olası bir çıktı değildir - ancak minimum ek yük sağlar. (Bu önemsiz örnekte, sonuç tam gelişmiş sıralı tutarlılık ile aynıdır; daha büyük bir programda olmazdı.)
Elbette, görmek istediğiniz çıktıların
0 0 veya
37 17 olması durumunda, orijinal kodun etrafına bir muteks sarabilirsiniz. Ancak bu ana kadar okuduysanız, bunun nasıl çalıştığını zaten biliyorsunuzdur ve bu cevap zaten benim istediğimden daha uzun.
Öyleyse, en alt satırda. Muteksler mükemmeldir ve C ++ 11 onları standart hale getirir. Ancak bazen performans nedenlerinden dolayı daha düşük seviyeli ilkeller istersiniz (örneğin, klasik çift denetimli kilitleme düzeni ). Yeni standart, muteksler ve koşul değişkenleri gibi yüksek seviyeli araçlar sunar ve ayrıca atomik tipler ve çeşitli hafıza bariyerleri gibi düşük seviyeli araçlar sunar. Şimdi, tamamen standart tarafından belirtilen dilde karmaşık, yüksek performanslı eşzamanlı rutinler yazabilirsiniz ve kodunuzun hem bugünün sistemlerinde hem de yarınınkilerinde değişmeden çalışacağından emin olabilirsiniz.
Her ne kadar dürüst olsanız da, bir uzman olmadığınız ve ciddi bir düşük seviye kod üzerinde çalışmadığınız sürece, muhtemelen muteks ve koşul değişkenlerine bağlı kalmalısınız. Yapmayı düşündüğüm şey bu.
Bu konu hakkında daha fazla bilgi için bkz. bu blog sonrası .
Yalnızca bellek tutarlılık modellerini (ya da kısaca bellek modellerini) anladığım analojiyi vereceğim. Leslie Lamport'un seminal makalesinden ilham alan "Zaman, Saatler ve Dağıtılmış Bir Olayların Sıralanması Sistem ". Analoji uygun ve temel bir öneme sahip, ancak birçok kişi için aşırı olabilir. Ancak, umarım bellek tutarlılığı modelleri hakkında akıl yürütmeyi kolaylaştıran zihinsel bir görüntü (resimsel bir temsildir) sağlar.
Tüm bellek konumlarının tarihçelerini, yatay eksenin adres alanını temsil ettiği (yani her bellek konumunun o eksen üzerindeki bir noktayla temsil edildiği) ve dikey eksenin zamanı temsil ettiği (zamana göre Genel olarak, zamanın evrensel bir kavramının olmadığını görüyoruz). Bu nedenle, her bir hafıza konumunda tutulan değerlerin geçmişi, bu hafıza adresindeki dikey bir sütunla temsil edilir. Her değer değişikliği, konulara yeni bir değer yazan konu başlığından kaynaklanmaktadır. Bellek resmi ile, belirli bir zamanda gözlenebilir tüm gözlemlenebilir tüm bellek konumlarının değerlerinin toplamı /birleşimi anlamına gelir belirli bir konuya göre.
"Bellek Tutarlılığı ve Önbellek Tutarlılığı Üzerine Bir Astar" 'dan alıntı yapmak
Sezgisel (ve en kısıtlayıcı) bellek modeli, çok parçacıklı bir uygulamanın, her bir dişlinin sıralı uygulamalarının bir araya getirilmesi gibi görünmesi gereken, sanki tek bir çekirdekte zamana göre çoğaltılmış gibi görünmesi gereken sıralı tutarlılıktır (SC). işlemci.
Genel bellek düzeni, programın bir çalışmasından diğerine değişebilir ve önceden bilinemeyebilir. SC'nin karakteristik özelliği, adres-yer-zaman diyagramındaki eşzamanlılık düzlemlerini (yani hafıza görüntüleri) temsil eden yatay dilimler kümesidir. Belirli bir düzlemde, tüm olayları (veya hafıza değerleri) aynı andadır. Tüm iş parçacıklarının hangi bellek değerlerinin eşzamanlı olduğu konusunda hemfikir olduğu bir Mutlak Zaman kavramı vardır. SC'de, her anda, tüm iş parçacıkları tarafından paylaşılan yalnızca bir bellek görüntüsü vardır. Bu, her zaman, tüm işlemciler bellek görüntüsü üzerinde anlaşır (yani, belleğin toplam içeriği). Bu, tüm iş parçacıklarının tüm bellek konumları için aynı değer sırasını görüntülediği anlamına gelmez, aynı zamanda tüm işlemcilerin tüm değişkenlerin aynı değer bileşimlerini de gözlemledikleri anlamına gelir. Bu, tüm bellek işlemlerinin (tüm bellek konumlarında) tüm iş parçacıkları tarafından aynı sırayla gözlemlendiğini söylemekle aynıdır.
Rahat bellek modellerinde, her iş parçacığı adres-boşluk-zamanını kendi yoluna göre keser, tek kısıtlama her iş parçacığının dilimlerinin birbirini geçmemesidir, çünkü tüm iş parçacıkları her bir ayrı bellek konumunun geçmişi üzerinde anlaşmalıdır. (tabii ki, farklı ipliklerin dilimleri birbirini geçebilir ve geçecektir). Dilimlemenin evrensel bir yolu yoktur (adres-mekan-zamanın ayrıcalıklı bir ayrıntısı yoktur). Dilimler düzlemsel (veya doğrusal) olmak zorunda değildir. Bunlar kavisli olabilir ve bu, bir iş parçacığının başka bir iş parçacığı tarafından yazılan sırayla yazılan değerleri okumalarını sağlayabilir. Farklı bellek konumlarının geçmişi birbirleriyle göreceli olarak rasgele kayabilir (ya da uzayabilir) Belirli bir konu tarafından görüntülendiğinde . Her iş parçacığı, hangi olayların (veya eşdeğerde, bellek değerlerinin) eşzamanlı olduğu farklı bir anlayışa sahip olacaktır. Bir iş parçacığına eşzamanlı olan olaylar kümesi (veya hafıza değerleri) diğerine eşzamanlı değildir. Bu nedenle, rahat bir bellek modelinde, tüm dişler hala her bir hafıza yeri için aynı tarihi (yani değer dizisi) izler. Ancak farklı bellek görüntülerini gözlemleyebilirler (yani tüm bellek konumlarının değerlerinin birleşimi). Aynı iş parçacığı tarafından iki farklı bellek konumu sırayla yazılsa bile, yeni yazılan iki değer diğer iş parçacıkları tarafından farklı sırayla görülebilir.
[Wikipedia'dan Resim]
Einstein’ın Özel Görelilik Teorisi hakkında bilgi sahibi olan okuyucular, neye benzediğimi anlayacaklardır. Minkowski’nin sözcüklerini bellek modellerine çevirmek: adres alanı ve zaman adres-zaman-zaman gölgesidir. Bu durumda, her gözlemci (iş parçacığı) olayların gölgelerini (yani hafıza depolar /yükler) kendi dünya çizgisine (yani zaman ekseni) ve kendi eşzamanlılık düzlemine (adres-uzay ekseni) yansıtacaktır. . C ++ 11 bellek modelindeki başlıklar, özel görelilik açısından birbirine göre hareket eden observers 'e karşılık gelir. Sıralı tutarlılık, Galilean uzay-zamanı 'na tekabül eder (yani tüm gözlemciler bir mutlak olay sırası ve küresel bir simultanelik duygusu üzerinde anlaşırlar).
Bellek modelleri ve özel görelilik arasındaki benzerlik, her ikisi de çoğunlukla nedensel bir küme olarak adlandırılan, kısmen sıralı bir olay kümesini tanımlamasından kaynaklanır. Bazı olaylar (yani hafıza depoları) diğer olayları etkileyebilir (ancak bundan etkilenmez). Bir C ++ 11 dizisi (veya fizikteki gözlemci), bir olay zincirinden (yani, tamamen düzenlenmiş bir dizi) (örn., Bellek yükler ve muhtemelen farklı adreslere depolanır) bir şey değildir.
İzafiyette, bazı sıralamalar kısmen sıralanan olayların görünüşte kaotik görüntüsüne kavuşur, çünkü tüm gözlemcilerin aynı fikirde olduğu tek geçici sıra, "zaman çizelgesi" olayları arasındaki sıralamadır (yani, herhangi biri tarafından prensipte bağlanabilir olan olaylar) partikül, vakumdaki ışık hızından daha yavaş ilerliyor). Yalnızca zaman çizelgesiyle ilgili olaylar değişmez şekilde sıralanır. Fizik Zamanı, Craig Callender .
C ++ 11 bellek modelinde, bu yerel nedensellik ilişkilerini kurmak için benzer bir mekanizma (edinim-yayım tutarlılık modeli) kullanılır.
SC'nin bırakılması için bir bellek tutarlılığı tanımı ve motivasyon sağlamak için adresinden alıntı yapacağım "Bellek Tutarlılığı ve Önbellek Tutarlılığı Üzerine Bir Astar"
Paylaşılan bir bellek makinesi için, bellek tutarlılığı modeli, bellek sisteminin mimari olarak görünür davranışını tanımlar. Tek bir işlemci çekirdeği için doğruluk ölçütü, “ bir doğru sonuç ” ve “ birçok yanlış alternatif ” arasındaki davranışı bölüştürür. Bunun nedeni, işlemcinin mimarisinin bir iş parçacığının yürütülmesinin belirli bir girdi durumunu sıra dışı bir çekirdekte bile iyi bir tek tanımlanmış çıktı durumuna dönüştürmesini gerektirmesidir. Ancak paylaşılan bellek tutarlılığı modelleri, birden fazla iş parçacığının yükleri ve depoları ile ilgilidir ve çoğu (daha fazla) yanlış olanı reddetmekle birlikte genellikle birçok doğru uygulamaya izin verir. Çoklu doğru uygulamaların olasılığı, ISA'nın çoklu iş parçacıklarının eşzamanlı olarak çalıştırılmasına izin vermesinden kaynaklanır, çoğu zaman farklı iş parçacıklarından gelen talimatların olası birçok olası bir araya getirilmesiyle.
Relaxed veya weak bellek tutarlılığı modelleri, güçlü modellerdeki çoğu bellek siparişinin gereksiz. Bir iş parçacığı on veri öğesini ve ardından bir senkronizasyon bayrağını güncellerse, programcılar genellikle veri öğelerinin birbirine göre güncellenip güncellenmediğini önemsemez, ancak yalnızca bayrak güncellenmeden önce tüm veri öğelerinin güncellenmesini önemser (genellikle FENCE talimatları kullanılarak uygulanır) ). Rahat modeller bu artan sipariş esnekliğini yakalamaya çalışır ve SC'nin hem daha yüksek performansını hem de doğruluğunu elde etmek için yalnızca programcıların “ gerektirdiği ” siparişleri korur. Örneğin, bazı mimarilerde, FIFO yazma arabellekleri, sonuçları önbelleklere yazmadan önce, her çekirdek tarafından taahhütlü (emekli) mağazaların sonuçlarını tutmak için kullanılır. Bu optimizasyon performansı arttırır ancak SC'yi ihlal eder. Yazma arabelleği, bir mağazanın servisini kaybetme gecikmesini gizler. Mağazalar yaygın olduğu için, çoğuna engel olmaktan kaçınmak önemli bir avantajdır. Tek çekirdekli bir işlemci için, bir A adresine bir yükün, yazma arabelleğinde bir veya daha fazla mağaza olmasına rağmen, en son mağazanın değerini A'ya döndürmesini sağlayarak mimari olarak görünmez hale getirilebilir. Bu genellikle, en son mağazanın değerinin A'ya A'dan yüke atlanmasıyla, "en son" program sırasına göre belirlendiğinde ya da A için bir mağazanın yazma arabelleğindeyse bir A yükünün durmasıyla yapılır. . Birden çok çekirdek kullanıldığında, her biri kendi kendine yazarak yazma arabelleğine sahip olacaktır. Yazma arabellekleri olmadan, donanım SC'dir, ancak yazma arabellekleriyle, yazma arabelleklerini çok çekirdekli bir işlemcide mimari olarak görünür kılar.
Bir çekirdeğin mağazadan ayrılmasını sağlayan FIFO olmayan bir yazma arabelleğine sahip olması durumunda mağaza-mağaza yeniden sıralama olabilirn girdikleri sıradan farklı bir sıra. Bu durum, ikinci vuruş vururken ilk mağaza önbelleği kaçırırsa veya ikinci mağaza daha önceki bir mağazayla birleşebilirse (örneğin, ilk mağazadan önce) oluşabilir. Yük yükleme yeniden sıralaması, program sırasındaki yönergeleri yürüten dinamik olarak zamanlanmış çekirdeklerde de olabilir. Bu, başka bir çekirdekte depoları yeniden düzenlemekle aynı şekilde davranabilir (İki iplik arasında bir araya getirme örneği bulabilir misin?). Daha eski bir yükü daha sonra bir deposuyla (bir yük deposu yeniden sıralaması) yeniden sıralamak, onu koruyan kilidi serbest bıraktıktan sonra bir değer yüklemek gibi birçok yanlış davranışa neden olabilir (mağaza kilit açma işlemi ise). Depo yükleme yeniden sıralamasının, tüm komutları program sırasına uygulayan bir çekirdeğe sahip olsa bile, yaygın olarak uygulanan FIFO yazma arabelleğindeki yerel bypass işleminden kaynaklanabileceğini unutmayın.
Önbellek tutarlılığı ve hafıza tutarlılığı bazen karışık olduğundan, bu alıntıya da sahip olmak öğreticidir:
Tutarlılıktan farklı olarak, önbellek tutarlılığı ne yazılım tarafından görünür ne de zorunludur. Tutarlılık, paylaşılan bellek sisteminin önbelleklerini tek çekirdekli bir sistemdeki önbellekler kadar işlevsel olarak görünmez kılmaya çalışır. Doğru tutarlılık, bir programcının yüklerin ve depoların sonuçlarını analiz ederek bir sistemin önbelleğe girip girmediğini ve nerede olacağını tespit edememesini sağlar. Bunun nedeni, doğru tutarlılığın, önbelleklerin hiçbir zaman yeni veya farklı functional davranışını etkinleştirmemesini sağlamasıdır (programcılar zamanlamayı
önbellek yapısına neden olabilir. /em>bilgi). Önbellek tutarlılık protokollerinin temel amacı, tek yazarlı çoklu okuyucunun (SWMR) her bellek yeri için değişmez olmasını sağlamaktır. Tutarlılık ve tutarlılık arasındaki önemli bir ayrım, tutarlılığın her bir yer için temel ile belirtilmiş olmasıdır; oysa tutarlılık ise tümü hafıza yerleri.
Zihinsel tablomuza devam edersek, SWMR değişmezi herhangi bir yerde en fazla bir partikül bulunması fiziksel gereksinimine karşılık gelir, ancak herhangi bir yerde sınırsız sayıda gözlemci olabilir.
Bu şimdi çok yıllık bir sorudur, ancak çok pop olmakUlar, C ++ 11 bellek modeli hakkında bilgi edinmek için harika bir kaynaktan bahsetmeye değer. Bunu bir başka tam cevap haline getirmek için konuşmasını özetlemenin hiçbir anlamı görmüyorum, ancak bu verilen standardı yazan kişi olduğu için, konuşmayı izlemeye değeceğini düşünüyorum.
Herb Sutter, Channel9 sitesinde bulunan "atomik &Silahlar" adlı C ++ 11 bellek modeli hakkında üç saatlik bir konuşma yaptı. bölüm 1 ve bölüm 2 . Konuşma oldukça teknik ve aşağıdaki konuları içeriyor:
- Optimizasyonlar, Yarışlar ve Bellek Modeli
- Sipariş Verme - Ne: Alma ve Bırakma
- Sipariş Verme - Nasıl: Mutexes, Atomics ve /veya Çitler
- Derleyici ve Donanımdaki Diğer Kısıtlamalar
- Kod Gen & Performans: x86 /x64, IA64, GÜÇ, KOL
- Rahat Atomlar
Konuşma, API üzerinde ayrıntılı bir şekilde açıklanmıyor, bunun yerine, başlık altında ve sahnelerin arkasında akıl yürütme, arka plan üzerinde çalışıyor (rahat anlambilimin yalnızca standartlara eklendiğini biliyor muydunuz, çünkü POWER ve ARM senkronize yükü verimli bir şekilde desteklemiyor ?).