C ++ 11 wprowadził ustandaryzowany model pamięci. Co to znaczy? Jak to wpłynie na programowanie w C ++?


1894

C ++ 11 wprowadził ustandaryzowany model pamięci, ale co to dokładnie znaczy? Jak to wpłynie na programowanie w C ++?

Ten artykuł (autorstwa Gavina Clarke'a, który cytuje Herb Sutter ) mówi, że:

Model pamięci oznacza, że ​​kod C ++ ma teraz znormalizowaną bibliotekę do wywołania, niezależnie od tego, kto stworzył kompilator i na jakiej platformie jest uruchomiony. Istnieje standardowy sposób kontrolowania, w jaki sposób różne wątki komunikują się z pamięcią procesora.

„Kiedy mówisz o dzieleniu [kodu] na różne rdzenie, które są w standardzie, mówimy o modelu pamięci. Zamierzamy go zoptymalizować bez złamania następujących założeń, które ludzie przyjmą w kodzie” - powiedział Sutter .

Cóż, mogę zapamiętać ten i podobne akapity dostępne w Internecie (ponieważ mam swój własny model pamięci od urodzenia: P), a nawet mogę pisać jako odpowiedź na pytania zadane przez innych, ale szczerze mówiąc, nie do końca rozumiem to.

Programiści C ++ już wcześniej opracowywali aplikacje wielowątkowe, więc jakie to ma znaczenie, jeśli są to wątki POSIX, Windows lub C ++ 11? Jakie są korzyści? Chcę zrozumieć szczegóły niskiego poziomu.

Mam również wrażenie, że model pamięci C ++ 11 jest w jakiś sposób powiązany z obsługą wielowątkowości C ++ 11, ponieważ często widzę te dwa razem. Jeśli tak, to jak dokładnie? Dlaczego powinny być powiązane?

Ponieważ nie wiem, jak działają elementy wewnętrzne wielowątkowości i co ogólnie oznacza model pamięci, pomóż mi zrozumieć te pojęcia. :-)


3
@curiousguy: Elaborate ...
Nawaz

4
@curiousguy: Napisz blog, a następnie ... i zaproponuj poprawkę. Nie ma innego sposobu, aby twój punkt był ważny i uzasadniony.
Nawaz

2
Pomyliłem tę stronę jako miejsce, w którym można zapytać Q i wymienić pomysły. Mój błąd; jest to miejsce na zgodność, w którym nie można nie zgodzić się z Herbem Sutterem, nawet jeśli rażąco zaprzecza on specyfikacji rzutu.
ciekawy,

5
@curiousguy: C ++ jest tym, co mówi Standard, a nie tym, co mówi przypadkowy facet w Internecie. Więc tak, musi być zgodna ze standardem. C ++ NIE jest otwartą filozofią, w której można mówić o wszystkim, co nie jest zgodne ze standardem.
Nawaz

3
„Udowodniłem, że żaden program w C ++ nie może mieć dobrze zdefiniowanego zachowania.” . Wysokie roszczenia, bez żadnego dowodu!
Nawaz

Odpowiedzi:


2204

Najpierw musisz nauczyć się myśleć jak prawnik językowy.

Specyfikacja C ++ nie zawiera odniesienia do żadnego konkretnego kompilatora, systemu operacyjnego lub procesora. Odwołuje się do abstrakcyjnej maszyny, która jest uogólnieniem rzeczywistych systemów. W świecie Language Lawyer zadaniem programisty jest pisanie kodu dla abstrakcyjnej maszyny; zadaniem kompilatora jest aktualizacja tego kodu na konkretnej maszynie. Kodując sztywno zgodnie ze specyfikacją, możesz mieć pewność, że Twój kod będzie się kompilował i działał bez modyfikacji w dowolnym systemie z kompatybilnym kompilatorem C ++, zarówno dzisiaj, jak i za 50 lat.

Maszyna abstrakcyjna w specyfikacji C ++ 98 / C ++ 03 jest zasadniczo jednowątkowa. Dlatego nie jest możliwe napisanie wielowątkowego kodu C ++, który jest „w pełni przenośny” w odniesieniu do specyfikacji. Specyfikacja nawet nie mówi nic o atomowości ładowań i magazynów pamięci ani o kolejności, w której mogą się zdarzać ładunki i sklepy, nie wspominając o takich rzeczach jak muteksy.

Oczywiście możesz pisać kod wielowątkowy w praktyce dla konkretnych konkretnych systemów - takich jak pthreads lub Windows. Ale nie ma standardowego sposobu pisania kodu wielowątkowego dla C ++ 98 / C ++ 03.

Maszyna abstrakcyjna w C ++ 11 jest wielowątkowa z założenia. Ma również dobrze zdefiniowany model pamięci ; oznacza to, co kompilator może, a czego nie może zrobić, jeśli chodzi o dostęp do pamięci.

Rozważ następujący przykład, w którym para zmiennych globalnych jest dostępna jednocześnie przez dwa wątki:

           Global
           int x, y;

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

Co może wygenerować wątek 2?

W C ++ 98 / C ++ 03 nie jest to nawet niezdefiniowane zachowanie; samo pytanie nie ma znaczenia, ponieważ standard nie uwzględnia niczego zwanego „wątkiem”.

W C ++ 11 wynikiem jest zachowanie niezdefiniowane, ponieważ ładunki i zapasy nie muszą być ogólnie atomowe. Co może nie wydawać się dużą poprawą ... I samo w sobie nie jest.

Ale w C ++ 11 możesz napisać to:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Teraz sprawy stają się znacznie bardziej interesujące. Po pierwsze, zachowanie tutaj jest zdefiniowane . Wątek 2 może teraz zostać wydrukowany 0 0(jeśli działa przed wątkiem 1), 37 17(jeśli działa po wątku 1) lub 0 17(jeśli działa po tym, jak wątek 1 przypisuje x, ale przed przypisaniem do y).

Nie może drukować 37 0, ponieważ domyślnym trybem dla ładunków / magazynów atomowych w C ++ 11 jest wymuszanie spójności sekwencyjnej . Oznacza to po prostu, że wszystkie ładunki i magazyny muszą być „tak, jakby” miały miejsce w kolejności, w jakiej zostały napisane w każdym wątku, podczas gdy operacje między wątkami mogą być przeplatane w dowolny sposób. Tak więc domyślne zachowanie atomiki zapewnia zarówno atomowość, jak i porządkowanie ładunków i zapasów.

Teraz na nowoczesnym procesorze zapewnienie sekwencyjnej spójności może być kosztowne. W szczególności kompilator najprawdopodobniej będzie emitował pełne bariery pamięciowe między każdym dostępem tutaj. Ale jeśli twój algorytm może tolerować ładunki i zamówienia poza kolejnością; tzn. jeśli wymaga atomowości, ale nie porządkowania; tzn. jeśli może tolerować 37 0dane wyjściowe z tego programu, możesz napisać to:

           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;

Im bardziej nowoczesny procesor, tym większe prawdopodobieństwo, że będzie to szybsze niż w poprzednim przykładzie.

Na koniec, jeśli chcesz zachować porządek w poszczególnych ładunkach i sklepach, możesz napisać:

           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;

To zabiera nas z powrotem do zamówionych ładunków i sklepów - więc 37 0nie jest to już możliwe wyjście - ale robi to przy minimalnym obciążeniu. (W tym trywialnym przykładzie wynik jest taki sam, jak pełna zgodność sekwencyjna; w większym programie tak nie byłoby).

Oczywiście, jeśli jedynymi wyjściami, które chcesz zobaczyć, są 0 0lub 37 17, możesz po prostu owinąć muteks wokół oryginalnego kodu. Ale jeśli przeczytałeś do tej pory, założę się, że wiesz już, jak to działa, a ta odpowiedź jest już dłuższa niż zamierzałem :-).

Więc dolna linia. Muteksy są świetne, a C ++ 11 je standaryzuje. Ale czasami ze względów wydajnościowych potrzebujesz prymitywów niższego poziomu (np. Klasyczny wzorzec blokowania z podwójną kontrolą ). Nowy standard zapewnia gadżety wysokiego poziomu, takie jak muteksy i zmienne warunkowe, a także zapewnia gadżety niskiego poziomu, takie jak typy atomowe i różne bariery pamięci. Teraz możesz pisać skomplikowane, wysokowydajne współbieżne procedury całkowicie w języku określonym przez standard, i możesz mieć pewność, że Twój kod będzie się kompilował i działał bez zmian zarówno w dzisiejszych systemach, jak i w przyszłości.

Chociaż szczerze mówiąc, chyba że jesteś ekspertem i pracujesz nad poważnym kodem niskiego poziomu, prawdopodobnie powinieneś trzymać się muteksów i zmiennych warunkowych. To właśnie zamierzam zrobić.

Aby uzyskać więcej informacji na ten temat, zobacz ten post na blogu .


37
Dobra odpowiedź, ale tak naprawdę błagam o kilka przykładów nowych prymitywów. Ponadto myślę, że kolejność pamięci bez operacji podstawowych jest taka sama, jak w wersji sprzed C ++ 0x: nie ma żadnych gwarancji.
John Ripley,

5
@John: Wiem, ale wciąż uczę się prymitywów :-). Myślę też, że gwarantują one, że dostęp do bajtów jest atomowy (choć nie zamówiony), dlatego w moim przykładzie użyłem „char” ... Ale nie jestem nawet w 100% pewien ... Jeśli chcesz zasugerować coś dobrego ” samouczek ”referencje dodam je do mojej odpowiedzi
Nemo

48
@Nawaz: Tak! Dostęp do pamięci może zostać zmieniony przez kompilator lub procesor. Pomyśl o (np.) Pamięciach podręcznych i ładunkach spekulacyjnych. Kolejność trafiania pamięci systemowej może być inna niż kodowana. Kompilator i procesor zapewnią, że takie zmiany kolejności nie spowodują uszkodzenia kodu jednowątkowego . W przypadku kodu wielowątkowego „model pamięci” charakteryzuje możliwe zmiany kolejności oraz to, co dzieje się, gdy dwa wątki odczytują / zapisują tę samą lokalizację w tym samym czasie, oraz sposób sprawowania kontroli nad obiema. W przypadku kodu jednowątkowego model pamięci nie ma znaczenia.
Nemo,

26
@Nawaz, @Nemo - Drobny szczegół: nowy model pamięci jest istotny w kodzie jednowątkowym, o ile określa niezdefiniowanie niektórych wyrażeń, takich jak i = i++. Stara koncepcja punktów sekwencji została odrzucona; nowy standard określa to samo, wykorzystując relację sekwencyjną przed, która jest tylko szczególnym przypadkiem bardziej ogólnej koncepcji między wątkami, która wydarzyła się przed .
JohannesD

17
@ AJG85: Sekcja 3.6.2 projektu specyfikacji C ++ 0x mówi: „Zmienne ze statycznym czasem przechowywania (3.7.1) lub czasem przechowywania wątków (3.7.2) powinny być inicjowane zerem (8.5) przed jakąkolwiek inną inicjalizacją miejsce." Ponieważ x, y są globalne w tym przykładzie, mają one statyczny czas przechowywania i dlatego, jak sądzę, zostaną zainicjowane przez zero.
Nemo,

345

Podam tylko analogię, z którą rozumiem modele spójności pamięci (lub modele pamięci, w skrócie). Inspiracją jest przełomowy artykuł Leslie Lamporta „Czas, zegary i kolejność zdarzeń w systemie rozproszonym” . Analogia jest trafna i ma fundamentalne znaczenie, ale dla wielu osób może być przesadą. Mam jednak nadzieję, że zapewnia obraz mentalny (przedstawienie obrazowe), który ułatwia rozumowanie modeli spójności pamięci.

Zobaczmy historie wszystkich lokalizacji pamięci na schemacie czasoprzestrzennym, w którym oś pozioma reprezentuje przestrzeń adresową (tj. Każda lokalizacja pamięci jest reprezentowana przez punkt na tej osi), a oś pionowa reprezentuje czas (zobaczymy, ogólnie nie ma uniwersalnego pojęcia czasu). Historia wartości przechowywanych w każdej lokalizacji pamięci jest zatem reprezentowana przez pionową kolumnę pod tym adresem pamięci. Każda zmiana wartości wynika z tego, że jeden z wątków zapisuje nową wartość w tej lokalizacji. Przez obraz pamięci rozumiemy agregację / kombinację wartości wszystkich lokalizacji pamięci, które można zaobserwować w określonym czasie przez określony wątek .

Cytując z „Podstawy spójności pamięci i spójności pamięci podręcznej”

Intuicyjny (i najbardziej restrykcyjny) model pamięci to spójność sekwencyjna (SC), w której wykonanie wielowątkowe powinno wyglądać jak przeplatanie sekwencji wykonywania każdego z wątków składowych, tak jakby wątki były multipleksowane czasowo w procesorze jedno-rdzeniowym.

Ta globalna kolejność pamięci może się różnić w zależności od uruchomienia programu i może nie być wcześniej znana. Cechą charakterystyczną SC jest zestaw poziomych przekrojów na schemacie adres-czasoprzestrzeń reprezentujących płaszczyzny jednoczesności (tj. Obrazy pamięci). Na danej płaszczyźnie wszystkie jej zdarzenia (lub wartości pamięci) są równoczesne. Istnieje pojęcie czasu bezwzględnego , w którym wszystkie wątki zgadzają się, które wartości pamięci są równoczesne. W SC w każdej chwili istnieje tylko jeden obraz pamięci współużytkowany przez wszystkie wątki. Oznacza to, że w każdej chwili wszystkie procesory uzgadniają obraz pamięci (tj. Zagregowaną zawartość pamięci). Oznacza to nie tylko, że wszystkie wątki wyświetlają tę samą sekwencję wartości dla wszystkich lokalizacji w pamięci, ale także że wszystkie procesory obserwują to samokombinacje wartości wszystkich zmiennych. Jest to to samo, co stwierdzenie, że wszystkie operacje pamięci (we wszystkich lokalizacjach pamięci) są obserwowane w tej samej całkowitej kolejności przez wszystkie wątki.

W zrelaksowanych modelach pamięci, każdy wątek tnie adres-czasoprzestrzeń na swój sposób, jedynym ograniczeniem jest to, że wycinki każdego wątku nie powinny się przecinać, ponieważ wszystkie wątki muszą zgadzać się z historią każdego pojedynczego miejsca pamięci (oczywiście , plastry różnych nici mogą i będą się krzyżować). Nie ma uniwersalnego sposobu na podzielenie go na części (brak uprzywilejowanego foliowania czasu i przestrzeni adresowej). Plastry nie muszą być płaskie (ani liniowe). Mogą być zakrzywione, a to może sprawić, że wątek odczyta wartości zapisane przez inny wątek w kolejności, w jakiej zostały zapisane. Historie różnych lokalizacji pamięci mogą przesuwać się (lub rozciągać) dowolnie względem siebie, gdy są oglądane przez dowolny konkretny wątek. Każdy wątek będzie miał inne wyczucie, które zdarzenia (lub równoważnie wartości pamięci) są jednoczesne. Zestaw zdarzeń (lub wartości pamięci), które są jednoczesne dla jednego wątku, nie są jednoczesne dla drugiego. Tak więc w zrelaksowanym modelu pamięci wszystkie wątki nadal obserwują tę samą historię (tj. Sekwencję wartości) dla każdej lokalizacji pamięci. Mogą jednak obserwować różne obrazy pamięci (tj. Kombinacje wartości wszystkich lokalizacji pamięci). Nawet jeśli dwie różne lokalizacje pamięci są zapisywane przez ten sam wątek w sekwencji, dwie nowo zapisane wartości mogą być obserwowane w innej kolejności przez inne wątki.

[Zdjęcie z Wikipedii] Zdjęcie z Wikipedii

Czytelnicy zaznajomieni ze Specjalną Teorią Względności Einsteina zauważą, o czym mówię. Tłumaczenie słów Minkowskiego na dziedzinę modeli pamięci: przestrzeń adresowa i czas to cienie adresu-czasoprzestrzeni. W takim przypadku każdy obserwator (tj. Wątek) będzie rzutował cienie zdarzeń (tj. Zapisy / obciążenia pamięci) na własną linię świata (tj. Jego oś czasu) i własną płaszczyznę jednoczesności (jego oś adres-przestrzeń) . Wątki w modelu pamięci C ++ 11 odpowiadają obserwatorom poruszającym się względem siebie w szczególnej teorii względności. Spójność sekwencyjna odpowiada czasoprzestrzeni Galilejskiej (tzn. Wszyscy obserwatorzy zgadzają się co do jednej absolutnej kolejności zdarzeń i globalnego poczucia jednoczesności).

Podobieństwo między modelami pamięci a szczególną teorią względności wynika z faktu, że oba definiują częściowo uporządkowany zestaw zdarzeń, często nazywany zbiorem przyczynowym. Niektóre zdarzenia (tj. Magazyny pamięci) mogą wpływać na inne zdarzenia (ale nie mieć na nie wpływu). Wątek C ++ 11 (lub obserwator w fizyce) jest niczym więcej niż łańcuchem (tj. Całkowicie uporządkowanym zbiorem) zdarzeń (np. Ładuje pamięć i przechowuje pod możliwie różnymi adresami).

W teorii względności pewien porządek przywracany jest pozornie chaotycznemu obrazowi częściowo uporządkowanych zdarzeń, ponieważ jedynym porządkiem czasowym, na który wszyscy obserwatorzy się zgadzają, jest porządkowanie między zdarzeniami „podobnymi do czasu” (tj. Zdarzeniami, które w zasadzie można połączyć dowolną cząsteczką wolniej działającą niż prędkość światła w próżni). Niezmiennie porządkowane są tylko zdarzenia związane z czasem. Czas w fizyce, Craig Callender .

W modelu pamięci C ++ 11 podobny mechanizm (model spójności nabywania i uwalniania) jest używany do ustalenia tych lokalnych związków przyczynowości .

Aby podać definicję spójności pamięci i motywację do porzucenia SC, zacytuję z „Podstawy spójności pamięci i spójności pamięci podręcznej”

W przypadku maszyny pamięci współdzielonej model spójności pamięci określa architektonicznie widoczne zachowanie systemu pamięci. Kryterium poprawności zachowania partycji z pojedynczym procesorem między „ jednym poprawnym wynikiem ” a „ wieloma niepoprawnymi alternatywami ”. Wynika to z faktu, że architektura procesora nakazuje, aby wykonanie wątku przekształciło dany stan wejściowy w pojedynczy dobrze zdefiniowany stan wyjściowy, nawet w rdzeniu poza kolejnością. Modele spójności pamięci współużytkowanej dotyczą jednak obciążeń i zapasów wielu wątków i zwykle umożliwiają wiele poprawnych wykonańnie zezwalając na wiele (więcej) niepoprawnych. Możliwość wielokrotnego poprawnego wykonania wynika z faktu, że ISA pozwala na jednoczesne wykonywanie wielu wątków, często z wieloma możliwymi prawnymi przeplataniami instrukcji z różnych wątków.

Modele zrelaksowanej lub słabej spójności pamięci są motywowane przez fakt, że większość porządków pamięci w silnych modelach nie jest potrzebna. Jeśli wątek aktualizuje dziesięć elementów danych, a następnie flagę synchronizacji, programiści zwykle nie dbają o to, czy elementy danych są aktualizowane względem siebie, ale tylko o to, że wszystkie elementy danych są aktualizowane przed aktualizacją flagi (zwykle wdrażane przy użyciu instrukcji FENCE ). Zrelaksowane modele starają się uchwycić tę zwiększoną elastyczność zamawiania i zachować tylko te zamówienia, których „programiści” wymagają”, Aby uzyskać zarówno wyższą wydajność, jak i poprawność SC. Na przykład w niektórych architekturach bufory zapisu FIFO są używane przez każdy rdzeń do przechowywania wyników zatwierdzonych (wycofanych) sklepów przed zapisaniem wyników w pamięciach podręcznych. Ta optymalizacja poprawia wydajność, ale narusza SC. Bufor zapisu ukrywa opóźnienie obsługi braków w sklepie. Ponieważ sklepy są powszechne, możliwość uniknięcia przeciągnięcia na większości z nich jest ważną korzyścią. W przypadku procesora jednordzeniowego bufor zapisu można uczynić niewidocznym architektonicznie, zapewniając, że obciążenie adresu A zwraca wartość ostatniego magazynu do A, nawet jeśli jeden lub więcej magazynów do A znajduje się w buforze zapisu. Zazwyczaj odbywa się to poprzez ominięcie wartości najnowszego sklepu do A do obciążenia z A, gdzie „najnowszy” jest określony przez kolejność programów, lub przez przeciągnięcie obciążenia A, jeśli pamięć do A znajduje się w buforze zapisu. Gdy używanych jest wiele rdzeni, każdy będzie miał swój własny obejściowy bufor zapisu. Bez buforów zapisu sprzętem jest SC, ale z buforami zapisu tak nie jest, dzięki czemu bufory zapisu są architektonicznie widoczne w procesorze wielordzeniowym.

Zmiana kolejności sklepu-sklepu może się zdarzyć, jeśli rdzeń ma bufor zapisu inny niż FIFO, który pozwala sklepom wyjść w innej kolejności niż kolejność, w której zostały wprowadzone. Może się to zdarzyć, jeśli pierwszy sklep nie trafi do pamięci podręcznej, podczas gdy drugi trafia lub gdy drugi sklep może połączyć się z wcześniejszym sklepem (tj. Przed pierwszym sklepem). Ponowne uporządkowanie obciążenia może także nastąpić na dynamicznie zaplanowanych rdzeniach, które wykonują instrukcje poza kolejnością programu. Może to zachowywać się tak samo, jak zmiana kolejności sklepów na innym rdzeniu (czy możesz wymyślić przykład przeplatania dwóch wątków?). Zmiana kolejności wcześniejszego ładowania za pomocą późniejszego magazynu (zmiana kolejności magazynu ładowania) może powodować wiele niepoprawnych zachowań, takich jak ładowanie wartości po zwolnieniu blokady, która ją chroni (jeśli sklep jest operacją odblokowania).

Ponieważ spójność pamięci podręcznej i spójność pamięci są czasami mylone, pouczające jest również mieć ten cytat:

W przeciwieństwie do spójności, spójność pamięci podręcznej nie jest ani widoczna dla oprogramowania, ani wymagana. Koherencja ma na celu uczynienie pamięci podręcznej systemu pamięci współdzielonej tak funkcjonalnie niewidoczną, jak pamięć podręczną w systemie jednordzeniowym. Prawidłowa spójność zapewnia, że ​​programista nie może ustalić, czy i gdzie system ma pamięć podręczną, analizując wyniki obciążeń i zapasów. Wynika to z faktu, że poprawna spójność zapewnia, że ​​pamięci podręczne nigdy nie umożliwiają nowego lub innego zachowania funkcjonalnego (programiści mogą nadal być w stanie wywnioskować prawdopodobną strukturę pamięci podręcznej przy użyciu taktowaniaInformacja). Głównym celem protokołów koherencji pamięci podręcznej jest utrzymanie niezmiennika pojedynczego modułu zapisującego-wielu czytników (SWMR) dla każdej lokalizacji pamięci. Ważnym rozróżnieniem między spójnością a spójnością jest to, że spójność jest określana dla każdej lokalizacji pamięci , podczas gdy spójność jest określana w odniesieniu do wszystkich lokalizacji pamięci.

Kontynuując nasz obraz mentalny, niezmiennik SWMR odpowiada fizycznemu wymogowi, że w jednym miejscu może znajdować się co najwyżej jedna cząstka, ale może być nieograniczona liczba obserwatorów w dowolnym miejscu.


52
+1 za analogię ze szczególną teorią względności, sam próbowałem stworzyć tę samą analogię. Zbyt często widzę, że programiści badający kod wątkowy próbują zinterpretować zachowanie, ponieważ operacje w różnych wątkach zachodzące ze sobą przeplatają się w określonej kolejności, i muszę powiedzieć im, nie, w systemach wieloprocesorowych, pojęcie równoczesności między różnymi <s > ramki odniesień </s> jest teraz bez znaczenia. Porównanie ze szczególną teorią względności jest dobrym sposobem na to, by szanowali złożoność problemu.
Pierre Lebeaupin

71
Więc powinieneś dojść do wniosku, że Wszechświat jest wielordzeniowy?
Peter K

6
@PeterK: Dokładnie :) A oto bardzo ładna wizualizacja tego zdjęcia czasu autorstwa fizyka Briana Greene'a: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s To jest „Złudzenie czasu [pełny dokument]” w minucie 22 i 12 sekund.
Ahmed Nassar,

2
Czy to tylko ja, czy też zmienia model pamięci 1D (oś pozioma) na model pamięci 2D (płaszczyzny jednoczesności). Uważam to za nieco mylące, ale może dlatego, że nie jestem native speakerem ... Wciąż bardzo interesująca lektura.
Do widzenia SE

Zapomniałeś istotnej części: „ analizując wyniki obciążeń i zapasów ” ... bez korzystania z dokładnych informacji o czasie.
ciekawy,

115

Jest to obecnie wieloletnie pytanie, ale będąc bardzo popularnym, warto wspomnieć o fantastycznym zasobie do nauki o modelu pamięci C ++ 11. Nie widzę sensu w podsumowywaniu swojego przemówienia, aby uzyskać kolejną pełną odpowiedź, ale biorąc pod uwagę, że jest to facet, który napisał standard, uważam, że warto go obejrzeć.

Herb Sutter ma trzygodzinną rozmowę o modelu pamięci C ++ 11 zatytułowanym „Broń atomowa <>”, dostępnym na stronie Channel9 - część 1 i część 2 . Dyskusja jest dość techniczna i obejmuje następujące tematy:

  1. Optymalizacje, wyścigi i model pamięci
  2. Zamawianie - Co: Nabycie i wydanie
  3. Zamawianie - jak: muteksy, atomiki i / lub ogrodzenia
  4. Inne ograniczenia dotyczące kompilatorów i sprzętu
  5. Kod Gen & Performance: x86 / x64, IA64, POWER, ARM
  6. Relaxed Atomics

Dyskusja nie dotyczy API, ale rozumowania, tła, pod maską i za kulisami (czy wiesz, że łagodna semantyka została dodana do standardu tylko dlatego, że POWER i ARM nie obsługują wydajnie synchronizowanego obciążenia?).


10
Ta rozmowa jest naprawdę fantastyczna, warta 3 godzin, które spędzisz na oglądaniu.
ZunTzu

5
@ZunTzu: w większości odtwarzaczy wideo możesz ustawić prędkość na 1,25, 1,5 lub nawet 2 razy większą niż oryginał.
Christian Severin,

4
@eran, czy zdarzyło ci się mieć slajdy? linki na stronach dyskusji kanału 9 nie działają.
athos

2
@athos Nie mam ich, przepraszam. Spróbuj skontaktować się z kanałem 9, nie sądzę, aby usunięcie było celowe (domyślam się, że dostali link z Herb Sutter, opublikował jak jest, a później usunął pliki; ale to tylko spekulacja ...).
eran

75

Oznacza to, że standard definiuje teraz wielowątkowość i określa, co dzieje się w kontekście wielu wątków. Oczywiście ludzie używali różnych implementacji, ale to tak, jakby pytać, dlaczego powinniśmy mieć, std::stringkiedy wszyscy moglibyśmy korzystać z stringklasy domowej .

Kiedy mówisz o wątkach POSIX lub Windows, jest to trochę złudzeniem, ponieważ tak naprawdę mówisz o wątkach x86, ponieważ jest to funkcja sprzętowa do jednoczesnego działania. Model pamięci C ++ 0x daje gwarancje, niezależnie od tego, czy korzystasz z x86, ARM, MIPS , czy cokolwiek innego, co możesz wymyślić.


28
Wątki posiksowe nie są ograniczone do x86. Rzeczywiście, pierwsze systemy, na których zostały zaimplementowane, prawdopodobnie nie były systemami x86. Wątki Posix są niezależne od systemu i są ważne na wszystkich platformach Posix. Nie jest również prawdą, że jest to właściwość sprzętowa, ponieważ wątki Posix można również zaimplementować poprzez wielozadaniowość kooperacyjną. Ale oczywiście większość problemów z wątkami pojawia się tylko na sprzętowych implementacjach wątków (a niektóre nawet tylko na systemach wieloprocesorowych / wielordzeniowych).
celtschk

57

W przypadku języków, które nie określają modelu pamięci, piszesz kod języka i modelu pamięci określonego przez architekturę procesora. Procesor może zmienić kolejność dostępów do pamięci w celu zwiększenia wydajności. Tak więc, jeśli twój program ma wyścigi danych (wyścig danych jest możliwy, gdy wiele rdzeni / hiperwątków ma dostęp do tej samej pamięci jednocześnie), to twój program nie jest wieloplatformowy z powodu zależności od modelu pamięci procesora. Możesz zapoznać się z instrukcją oprogramowania Intel lub AMD, aby dowiedzieć się, w jaki sposób procesory mogą ponownie zamówić dostęp do pamięci.

Bardzo ważne jest to, że zamki (i semantyka współbieżności z blokowaniem) są zazwyczaj implementowane w sposób wieloplatformowy ... Więc jeśli używasz standardowych zamków w programie wielowątkowym bez wyścigów danych, nie musisz się martwić o modele pamięci między platformami .

Co ciekawe, kompilatory Microsoft dla C ++ posiadają semantykę / release dla volatile, która jest rozszerzeniem C ++ do radzenia sobie z brakiem modelu pamięci w C ++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs .80) .aspx . Biorąc jednak pod uwagę, że Windows działa tylko na x86 / x64, nie znaczy to wiele (modele pamięci Intel i AMD ułatwiają i wydajnie wdrażać semantykę pozyskiwania / wydawania w języku).


2
Prawdą jest, że kiedy została napisana odpowiedź, Windows działa tylko na x86 / x64, ale Windows działa w pewnym momencie na IA64, MIPS, Alpha AXP64, PowerPC i ARM. Dziś działa na różnych wersjach ARM, która różni się pamięcią od x86 i nigdzie nie jest tak wybaczająca.
Lorenzo Dematté,

Ten link jest nieco zepsuty (mówi „Dokumentacja wycofana z programu Visual Studio 2005” ). Chcesz go zaktualizować?
Peter Mortensen

3
To nie była prawda, nawet gdy napisano odpowiedź.
Ben

„jednoczesny dostęp do tej samej pamięci ” w celu uzyskania dostępu w sprzeczny sposób
ciekawy,

27

Jeśli używasz muteksów do ochrony wszystkich swoich danych, naprawdę nie powinieneś się martwić. Mutexy zawsze zapewniały wystarczające gwarancje porządku i widoczności.

Teraz, jeśli używałeś atomów lub algorytmów bez blokady, musisz pomyśleć o modelu pamięci. Model pamięci opisuje dokładnie, kiedy atomics zapewnia porządek i gwarancje widoczności, a także zapewnia przenośne ogrodzenia dla ręcznie kodowanych gwarancji.

Wcześniej atomika była wykonywana przy użyciu wewnętrznych funkcji kompilatora lub biblioteki wyższego poziomu. Ogrodzenia zostałyby wykonane przy użyciu instrukcji specyficznych dla procesora (bariery pamięci).


19
Problem wcześniej polegał na tym, że nie było czegoś takiego jak mutex (pod względem standardu C ++). Tak więc jedyne gwarancje, które otrzymałeś od producenta mutex, były w porządku, pod warunkiem, że nie przeportowałeś kodu (ponieważ niewielkie zmiany w gwarancjach są trudne do wykrycia). Teraz otrzymujemy gwarancje zapewniane przez standard, które powinny być przenośne między platformami.
Martin York

4
@ Martin: w każdym razie jedna rzecz to model pamięci, a druga to prymitywy atomowe i wątkowe działające na tym modelu pamięci.
ninjalj

4
Chodziło mi przede wszystkim o to, że wcześniej nie było w większości modelu pamięci na poziomie językowym, zdarzało się, że był to model pamięci procesora. Teraz istnieje model pamięci, który jest częścią podstawowego języka; OTOH, muteksy i tym podobne można zawsze robić jako bibliotekę.
ninjalj

3
Może to również stanowić prawdziwy problem dla osób próbujących napisać bibliotekę mutex. Kiedy procesor, kontroler pamięci, jądro, kompilator i „biblioteka C” są zaimplementowane przez różne zespoły, a niektóre z nich nie zgadzają się co do tego, jak to ma działać, cóż, czasami rzeczy my, programiści systemów, musimy zrobić, aby prezentacja ładnej fasady na poziomie aplikacji nie była wcale przyjemna.
zwolnienie

11
Niestety nie wystarczy chronić struktury danych za pomocą prostych muteksów, jeśli nie ma spójnego modelu pamięci w twoim języku. Istnieją różne optymalizacje kompilatora, które mają sens w kontekście jednowątkowym, ale gdy w grę wchodzi wiele wątków i rdzeni procesora, zmiana kolejności dostępu do pamięci i inne optymalizacje mogą dać nieokreślone zachowanie. Aby uzyskać więcej informacji, zobacz „Tematy nie mogą być realizowane jako biblioteka” Hans Boehm: citeseer.ist.psu.edu/viewdoc/...
exDM69

0

Powyższe odpowiedzi dotyczą najbardziej podstawowych aspektów modelu pamięci C ++. W praktyce większość zastosowaństd::atomic<> „po prostu działa”, przynajmniej do momentu nadmiernej optymalizacji programisty (np. Poprzez próbę rozluźnienia zbyt wielu rzeczy).

Jest jedno miejsce, w którym błędy są nadal powszechne: sekwencje blokują się . Na stronie https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf znajduje się doskonała i łatwa do odczytania dyskusja . Blokady sekwencji są atrakcyjne, ponieważ czytelnik unika pisania słowa blokującego. Poniższy kod oparty jest na rysunku 1 powyższego raportu technicznego i przedstawia wyzwania związane z implementacją blokad sekwencji w C ++:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

Na początku tak nieintuicyjna, jak się wydaje, data1i data2musi być atomic<>. Jeśli nie są atomowe, można je odczytać (w reader()) dokładnie w tym samym czasie, w którym są zapisane (w writer()). Według modelu pamięci C ++ jest to wyścig, nawet jeśli reader()tak naprawdę nigdy nie korzysta z danych . Ponadto, jeśli nie są atomowe, kompilator może buforować pierwszy odczyt każdej wartości w rejestrze. Oczywiście nie chcesz tego ... chcesz ponownie przeczytać w każdej iteracji whilepętlireader() .

Nie wystarczy też ich utworzyć atomic<>i uzyskać do nich dostęp memory_order_relaxed. Powodem tego jest to, że tylko odczyty seq (in reader()) są pobierane semantyki. Mówiąc prosto, jeśli X i Y są dostępami do pamięci, X poprzedza Y, X nie jest nabyciem ani zwolnieniem, a Y jest nabyciem, to kompilator może zmienić kolejność Y przed X. Jeśli Y był drugim odczytem seq, a X był odczyt danych, takie zmiany kolejności przerwałyby implementację blokady.

Artykuł podaje kilka rozwiązań. Ten, który dzisiaj ma najlepszą wydajność, prawdopodobnie używa tegoatomic_thread_fence się memory_order_relaxed przed drugim czytamy o seqlock. W artykule jest to rysunek 6. Nie odtwarzam tutaj kodu, ponieważ każdy, kto przeczytał do tej pory, naprawdę powinien przeczytać ten artykuł. Jest bardziej precyzyjny i kompletny niż ten post.

Ostatnią kwestią jest to, że wykonanie tego może być nienaturalne data zmienne atomowe . Jeśli nie możesz tego zrobić w kodzie, musisz być bardzo ostrożny, ponieważ rzutowanie z nieatomowego na atomowy jest legalne tylko dla typów pierwotnych. C ++ 20 powinien dodaćatomic_ref<> , co ułatwi rozwiązanie tego problemu.

Podsumowując: nawet jeśli uważasz, że rozumiesz model pamięci C ++, powinieneś być bardzo ostrożny przed uruchomieniem własnych blokad sekwencji.


-2

C i C ++ były definiowane za pomocą śladu wykonawczego dobrze utworzonego programu.

Teraz są w połowie zdefiniowane przez ślad wykonania programu, a w połowie a posteriori przez wiele porządków na obiektach synchronizujących.

Oznacza to, że te definicje języka nie mają żadnego sensu, ponieważ nie ma logicznej metody łączenia tych dwóch podejść. W szczególności zniszczenie muteksu lub zmiennej atomowej nie jest dobrze zdefiniowane.


Podzielam twoją zaciętą chęć ulepszenia projektowania języka, ale myślę, że twoja odpowiedź byłaby bardziej cenna, gdyby koncentrowała się na prostym przypadku, dla którego jasno i wyraźnie pokazałeś, jak to zachowanie narusza określone zasady projektowania języka. Następnie zdecydowanie zalecam, jeśli pozwolicie mi, podać w tej odpowiedzi bardzo dobrą argumentację dotyczącą trafności każdego z tych punktów, ponieważ zostaną one skontrastowane z istotnością ogromnych korzyści w zakresie wydajności postrzeganych przez projekt w C ++
Matias Haeussler

1
@MatiasHaeussler Myślę, że źle odczytałeś moją odpowiedź; Nie sprzeciwiam się tutaj definicji konkretnej funkcji C ++ (mam też wiele takich ostrych uwag krytycznych, ale nie tutaj). Twierdzę tutaj, że nie ma dobrze zdefiniowanej konstrukcji w C ++ (ani C). Cała semantyka MT jest kompletnym bałaganem, ponieważ nie masz już semantyki sekwencyjnej. (Wierzę, że Java MT jest zepsuta, ale mniej.) „Prostym przykładem” byłby prawie każdy program MT. Jeśli się nie zgadzasz, możesz odpowiedzieć na moje pytanie, jak udowodnić poprawność programów MT C ++ .
ciekawy,

Interesujące, myślę, że rozumiem więcej, co masz na myśli po przeczytaniu twojego pytania. Jeśli mam rację, mówisz o niemożności opracowania dowodów poprawności programów C ++ MT . W takim przypadku powiedziałbym, że dla mnie ma to ogromne znaczenie dla przyszłości programowania komputerowego, w szczególności dla pojawienia się sztucznej inteligencji. Chciałbym jednak również wskazać, że dla ogromnej większości osób zadających pytania na temat przepełnienia stosu, których nawet nie są świadomi, a nawet po zrozumieniu, co masz na myśli i zainteresowaniu się
Matias Haeussler,

1
„Czy pytania dotyczące możliwości demostabilności programów komputerowych powinny być publikowane w trybie przepełnienia stosu lub wymiany stosu (jeśli nie w żadnym, to gdzie?)?” Wydaje się, że jest to jedna z metod przepełnienia stosu, prawda?
Matias Haeussler,

1
@MatiasHaeussler 1) C i C ++ zasadniczo dzielą „model pamięci” zmiennych atomowych, muteksów i wielowątkowości. 2) Znaczenie ma w tym korzyści wynikające z posiadania „modelu pamięci”. Myślę, że korzyść jest zerowa, ponieważ model jest niesłuszny.
ciekawy
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.