C ++ 11 je uveo standardizirani memorijski model. Što to znači? I kako će to utjecati na C ++ programiranje?

C ++ 11 je uveo standardizirani memorijski model, ali što to točno znači? I kako će to utjecati na C ++ programiranje?

Ovaj članak ( Gavin Clark citira Herb Sutter ) to kaže

Memorijski model znači da C ++ kod sada ima standardiziranu knjižnicu za pozivanje, bez obzira tko je stvorio kompajler i na kojoj platformi radi. Postoji standardni način kontrole načina na koji različite niti razgovaraju s memorijom procesora.

"Kada govorite o odvajanju [koda] u različite jezgre koje su u standardu, govorimo o modelu memorije. Optimizirat ćemo ga bez kršenja sljedećih pretpostavki koje će ljudi napraviti u kodu", rekao je Satter .

Pa, sjećam se ovih i sličnih stavaka koji su dostupni na internetu (budući da sam imao svoj vlastiti model memorije od trenutka rođenja: P), pa čak mogu napisati odgovor na pitanja koja su drugi postavili, ali iskreno, nisam baš Razumijem ovo.

C ++ programeri su se koristili za razvoj višenitnih aplikacija čak i ranije, pa koliko je to važno ako su POSIX niti ili Windows niti ili C ++ 11 niti? Koje su prednosti? Želim razumjeti detalje niske razine.

Također smatram da je model memorije C ++ 11 nekako povezan s višestrukom podrškom za C ++ 11, budući da ih često vidim zajedno. Ako da, kako? Zašto bi trebali biti povezani?

Budući da ne znam kako radi s više niti i koji model memorije kao cjeline, molim vas da mi pomognete razumjeti ove koncepte. :-)

1625
12 июня '11 в 2:30 2011-06-12 02:30 Nawaz je postavljen 12. lipnja 2011. u 2:30 am 2011-06-12 02:30
@ 6 odgovora

Prvo, morate naučiti razmišljati kao odvjetnik po jeziku.

C ++ specifikacija se ne odnosi na bilo koji specifični kompajler, operativni sustav ili procesor. On se poziva na apstraktni stroj, koji je generalizacija stvarnih sustava. U svijetu odvjetnika, posao programera je pisanje koda za apstraktni stroj; posao prevodioca je implementirati ovaj kod na određenom stroju. Ako strogo kodirate specifikaciju, možete biti sigurni da će vaš kod biti kompiliran i pokrenuti nepromijenjen na bilo kojem sustavu s kompatibilnim C ++ prevodiocem, bilo danas ili nakon 50 godina.

Apstraktni stroj u C ++ 98 / C ++ 03 specifikaciji je u biti jednostruki. Stoga je nemoguće pisati C ++ kod s više navoja, koji se u potpunosti prenosi prema specifikaciji. Specifikacija ne govori čak ni o atomičnosti opterećenja i pohranjivanju memorije ili o redoslijedu učitavanja i pohranjivanja podataka, da ne spominjemo takve stvari kao što su muteksi.

Naravno, u praksi možete napisati višenitne kodove za određene sustave - na primjer, pthreads ili Windows. No, ne postoji standardni način pisanja višenitnog koda za C ++ 98 / C ++ 03.

Sažetak stroja u C ++ 11 multi-thread dizajnu. Također ima dobro definiran memorijski model; to znači da prevodilac može i ne može raditi kada je u pitanju pristup memoriji.

Razmotrite sljedeći primjer u kojem se par globalnih varijabli istovremeno pristupa s dvije niti:

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

Što bi Thread 2 mogla iznijeti?

U C ++ 98 / C ++ 03 to nije čak i neodređeno ponašanje; samo pitanje je besmisleno, jer standard ne smatra ništa što se naziva "nit".

U C ++ 11, rezultat je nedefinirano ponašanje, jer opterećenja i trgovine ne moraju uopće biti atomski. To se možda ne čini kao jako dobro poboljšanje ... I to samo po sebi.

Ali s C ++ 11 možete napisati sljedeće:

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

Sada sve postaje mnogo zanimljivije. Ponajprije, ovdje se definira ponašanje. Thread 2 sada može ispisati 0 0 (ako radi prije Thread 1), 37 17 (ako radi nakon Thread 1) ili 0 17 (ako se pokrene nakon Thread 1 dodjeljuje x, ali prije nego dodijeli y ).

Ono što ne može ispisati je 37 0 jer je zadani način za atomska opterećenja / pohranu u C ++ 11 osiguravanje dosljedne konzistentnosti. To znači da bi sva opterećenja i skladišta trebala biti “kao da”, dogodila su se redoslijedom kojim ste ih snimili u svakom streamu, dok se operacije između potoka mogu izmjenjivati, ali sustav je ugodan. Prema tome, zadano ponašanje Atomicsa osigurava i atomičnost i redoslijed učitavanja i pohranjivanja.

Sada, na modernom procesoru, osiguravanje dosljedne konzistentnosti može biti skupo. Konkretno, kompajler vjerojatno emitira full-scale memorijske barijere između svakog pristupa ovdje. Ali ako vaš algoritam može tolerirati neupravljana opterećenja i trgovine; odnosno ako zahtijeva atomičnost, ali ne naručuje; odnosno ako može izvaditi 37 0 kao izlaz iz ovog programa, možete napisati ovo:

  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; 

Što je procesor moderniji, to je vjerojatnije da će biti brži od prethodnog primjera.

Naposljetku, ako trebate samo zadržati određena opterećenja i trgovine u redu, možete napisati:

  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 nas vraća na uredno učitavanje i pohranu - tako da 37 0 više nije mogući izlaz, ali to čini s minimalnim opterećenjem. (U ovom trivijalnom primjeru rezultat je isti kao dosljednost u punoj veličini; u većem programu to se neće dogoditi).

Naravno, ako samo izlaze koje želite vidjeti, 0 0 ili 37 17 , možete jednostavno omotati mutex oko izvornog koda. Ali ako je pročitate, siguran sam da već znate kako to funkcionira, a ovaj odgovor je duži nego što sam mislio :-).

Dakle, dno crta. Mutexi su veliki, a C ++ 11 ih standardizira. Ali ponekad, iz razloga performansi, potrebni su primitivci niže razine (na primjer, klasični uzorak s provjerom dvostrukog zaključavanja ). Novi standard pruža gadgete na visokoj razini, kao što su mutexes i varijable stanja, a također nudi gadgete niskog nivoa, kao što su atomski tipovi i različite mogućnosti zaštite memorije. Dakle, sada možete napisati složene paralelne rutine visokih performansi u cijelosti u jeziku koji određuje standard, i možete biti sigurni da će se vaš kod prevesti i raditi bez promjena u današnjem i budućem.

Iako, da budem iskren, ako niste stručnjak i ne radite na nekom ozbiljnom kodu niske razine, vjerojatno biste se trebali držati muteksa i varijabilnih uvjeta. To je ono što namjeravam učiniti.

Pogledajte ovaj post za više detalja.

1877
12 июня '11 в 3:23 2011-06-12 03:23 odgovor je dao Nemo 12. lipnja 2011. u 3:23 2011-06-12 03:23

Ja ću samo dati analogiju s kojom razumijem konzistentni model memorije (ili model memorije, ukratko). Inspiriran je semantičkim tekstom Lesley Lamport "Vrijeme, sati i redoslijed događaja u raspodijeljenom sustavu". Analogija je relevantna i od temeljne je važnosti, ali može biti suvišna za mnoge ljude. Međutim, nadam se da to daje mentalnu sliku (grafički prikaz), što olakšava razmišljanje o modelima konzistentnosti memorije.

Omogućuje vam da pregledate povijest svih memorijskih lokacija u prostorno-vremenskom dijagramu, u kojem vodoravna os predstavlja adresni prostor (tj. Svaka memorijska stanica predstavljena je točkom na ovoj osi), a vertikalna os predstavlja vrijeme (vidjet ćemo da je općenito vrijeme) , nema univerzalnog pojma vremena). Prema tome, povijest vrijednosti pohranjenih u svakoj memorijskoj ćeliji predstavljena je okomitim stupcem na ovoj memorijskoj adresi. Svaka promjena vrijednosti posljedica je činjenice da jedna od niti zapisuje novu vrijednost u ovo mjesto. Pod slikom memorije, razumjet ćemo ukupnost / kombinaciju vrijednosti svih memorijskih lokacija koje se promatraju u određenom vremenu , koristeći određeni tok .

Citat "Osnivač dosljednosti i dosljednosti predmemorije"

Intuitivni (i najrestriktivniji) memorijski model je sekvencijalna konzistentnost (SC), u kojoj bi višekutna izvedba trebala izgledati kao izmjena uzastopnih izvršavanja svake složene niti, kao da su niti multipleksirane na jednojezgreni procesor.

Ovaj globalni memorijski nalog može varirati od jednog programa do drugog i možda nije unaprijed poznat. Karakteristična značajka SC-a je skup horizontalnih rezova u dijagramu adresa-prostor-vrijeme koji predstavlja ravnine simultanosti (tj. Slike u memoriji). Na ovoj ravnini svi su događaji (ili memorijske vrijednosti) istodobni. Postoji koncept apsolutnog vremena u kojem se sve niti slažu s kojim su memorijskim vrijednostima istodobne. U SC postoji samo jedna memorijska slika u isto vrijeme, zajednička za sve niti. To jest, u svakom trenutku svi procesori su konzistentni sa slikom memorije (tj. Agregatnim sadržajem memorije). To ne znači samo da sve niti prikazuju isti slijed vrijednosti za sva memorijska mjesta, već i da svi procesori izvode iste kombinacije vrijednosti za sve varijable. To je isto kao i da se sve operacije memorije (u svim memorijskim ćelijama) promatraju u istom redoslijedu svih niti.

U modelima s oslabljenom memorijom, svaka nit će odvojiti adresu-prostor-vrijeme na svoj način, jedino ograničenje je da se rezovi svakog toka ne sijeku međusobno, jer se sve niti moraju podudarati s poviješću svake pojedine memorijske ćelije (naravno, komada različitih niti mogu i će se međusobno ukrštati). Ne postoji univerzalni način za rezanje (bez privilegiranog lišavanja adresnog prostora-vremena). Rezovi ne bi trebali biti ravni (ili linearni). Oni mogu biti zakrivljeni, i to je ono što vrijednosti protoka čitanja koje je napisao drugi tok, od onoga u kojem su napisane, mogu učiniti. Priče različitih mjesta memorije mogu se slobodno pomicati (ili rastezati) u međusobnom odnosu kada gledate bilo koji određeni tok . Svaka nit će imati drugačiju ideju o tome koji su događaji (ili, ekvivalentno, memorijske vrijednosti) istovremeni. Skup događaja (ili memorijskih vrijednosti) koji su istodobno povezani s jednom strujom nisu istovremeni s drugima. Prema tome, u modelu oslabljene memorije, sve niti i dalje održavaju istu povijest (tj. Slijed vrijednosti) za svaku memorijsku lokaciju. Ali oni mogu promatrati različite slike memorije (tj. Kombinacije vrijednosti svih memorijskih lokacija). Čak i ako su dvije različite memorijske lokacije zabilježene od strane istog toka u slijedu, dvije novo zabilježene vrijednosti mogu se promatrati u drugom redoslijedu prema drugim tokovima.

[Ilustracija Wikipedije]

Čitatelji koji poznaju Einsteinsovu posebnu relativnost primijetit će ono o čemu govorim. Prijevod riječi Minkowskog u polje modela memorije: adresni prostor i vrijeme su sjene adresnog prostora-vremena. U tom slučaju, svaki promatrač (tj. Flow) će projicirati sjene događaja (tj. Pamti memorije / opterećenja) na svoju vlastitu liniju svijeta (tj. Njegovu vremensku os) i svoju vlastitu ravninu istovjetnosti (njegova os adresnog prostora). ) Teme u modelu memorije C ++ 11 odgovaraju promatračima koji se međusobno pomiču u posebnoj teoriji relativnosti. Dosljedna dosljednost odgovara Galilejskom prostoru-vremenu (tj. Svi promatrači se slažu oko jednog apsolutnog poretka događaja i globalnog osjećaja simultanosti).

border=0

Sličnost između modela memorije i posebne teorije relativnosti proizlazi iz činjenice da oba definiraju djelomično uređen skup događaja, koji se često naziva kauzalni skup. Neki događaji (tj. Pohrana) mogu utjecati na druge događaje (ali ne i na njih). Protok C ++ 11 (ili promatrač u fizici) nije ništa drugo nego lanac događaja (to jest Potpuno uređen skup) događaja (na primjer, memorijska opterećenja i pohranjuje na moguće različite adrese).

U teoriji relativnosti, neki red se vraća na naizgled kaotičnu sliku djelomično uređenih događaja, budući da je jedini vremenski poredak s kojim se svi promatrači slažu uređivanje među "privremenim događajima" (tj. Oni događaji koji se načelno mogu povezati s bilo kojom česticom) sporije od brzine svjetlosti u vakuumu). Samo su vremenski određeni događaji nepromjenjivi. Vrijeme u fizici, Craig Callender .

U modelu memorije C ++ 11, sličan mehanizam (model izdavanja konzistencije-oslobađanja) koristi se za uspostavljanje ovih lokalnih uzročnih veza .

Da bi se osiguralo određivanje redoslijeda memorije i motivacije odbijanja od strane SC-a, od Primera ću dati dosljednost memorije i dosljednost predmemorije

Za računalo s zajedničkom memorijom, model konzistentnosti memorije određuje arhitektonski vidljivo ponašanje njegovog memorijskog sustava. Kriterij ispravnosti jedne procesorske jezgre dijeli ponašanje između "jednog ispravnog rezultata" i "mnogih nepravilnih alternativa". To je zbog činjenice da arhitektura procesora osigurava da izvršenje niti pretvara navedeno ulazno stanje u jedno dobro definirano izlazno stanje, čak i na jezgri izvan redoslijeda. Međutim, modeli konzistentnosti s zajedničkom memorijom odnose se na opterećenja i pohrane više niti i obično dopuštaju mnogo točnih izvršenja, izbjegavajući mnoge (više) netočne. Mogućnost višestrukih točnih izvršenja je zbog činjenice da ISA omogućuje istodobno izvršavanje više niti, često s mnogim mogućim legitimnim presretanjima naredbi iz različitih niti.

Relaksirani ili slabi modeli konzistentnosti memorije motivirani su činjenicom da većina memorijskih naloga u jakim modelima nije potrebna. Ako tok ažurira deset elemenata podataka, a zatim zastavicu za sinkronizaciju, obično nije važno za programere da li se podatkovni elementi ažuriraju redom u odnosu jedan na drugi, i samo se svi elementi podataka ažuriraju prije ažuriranja zastavice (obično se provode pomoću instrukcije FENCE). Opušteni modeli skloni su uhvatiti tu povećanu fleksibilnost narudžbe i zadržati samo narudžbe koje programeri "zahtijevaju" kako bi dobili bolju izvedbu i SC točnost. Na primjer, u nekim arhitekturama, svaki kernel koristi FIFO međuspremnike za pisanje kako bi pohranio rezultate fiksnih (udaljenih) spremišta prije pisanja rezultata u predmemoriju. Ova optimizacija poboljšava performanse, ali krši SC. Spremnik za pohranu skriva kašnjenje u servisiranju propusnica trgovine. Budući da su trgovine uobičajene, izbjegavanje zaustavljanja većine njih važna je prednost. Za procesor s jednom jezgrom, međuspremnik za pisanje može biti arhitektonski nevidljiv, osiguravajući da učitavanje adrese A vraća najnoviju pohranu na A, čak i ako je jedna ili više memorija za A u međuspremniku za pisanje. To se obično radi premošćivanjem vrijednosti najnovijeg spremišta u A na opterećenje iz A, gdje se "posljednji" određuje redoslijedom programa ili zaustavljanjem opterećenja A, ako je pohrana A u međuspremniku za pisanje. , Bez međuspremnika za pisanje, hardver je SC, ali s buffersima za pisanje, to nije slučaj, što čini pisače koji su arhitektonski vidljivi u multi-core procesoru.

Promjena redoslijeda pohrane koja se temelji na trgovini može se dogoditi ako kernel ima drugi međuspremnik za pisanje osim FIFO-a koji omogućuje trgovinama da napuste u drugom redoslijedu nego u redoslijedu kojim su unesene. To se može dogoditi ako prva trgovina propusti predmemoriju, a druga - ili ako se druga trgovina može spojiti s prijašnjom pohranom (tj. Prije prve trgovine). Promjena redoslijeda opterećenja može se također pojaviti u dinamički planiranim kernelima koji izvršavaju upute iz programa. To se može ponašati na isti način kao i preraspodjela trgovina na drugoj jezgri (možete li zamisliti primjer izmjene između dva toka?). Preraspodjela ranog učitavanja, a zatim pohrana (promjena redoslijeda učitavanja pohrane) može dovesti do mnogih pogrešnih radnji, kao što je učitavanje vrijednosti nakon otpuštanja brave, koja ga štiti (ako je pohrana otključavanje). Primijetite da se preraspodjela u spremištu može dogoditi i zbog lokalnog indeksiranja u uobičajeno implementiranom FIFO međuspremniku za pisanje, čak i s kernelom, koji izvršava sve naredbe redoslijedom izvršavanja programa.

Budući da su konzistentnost i konzistentnost memorije ponekad zbunjeni, također je uputno imati ovaj citat:

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