Byla vydána verze 4.0 multiplatformního integrovaného vývojového prostředí (IDE) pro rychlý vývoj aplikaci (RAD) ve Free Pascalu Lazarus (Wikipedie). Přehled novinek v poznámkách k vydání. Využíván je Free Pascal Compiler (FPC) 3.2.2.
Podpora Windows 10 končí 14. října 2025. Připravovaná kampaň Konec desítek (End of 10) může uživatelům pomoci s přechodem na Linux.
Již tuto středu proběhne 50. Virtuální Bastlírna, tedy dle římského číslování L. Bude L značit velikost, tedy více diskutujících než obvykle, či délku, neboť díky svátku lze diskutovat dlouho do noci? Bude i příští Virtuální Bastlírna virtuální nebo reálná? Nejen to se dozvíte, když dorazíte na diskuzní večer o elektronice, softwaru, ale technice obecně, který si můžete představit jako virtuální posezení u piva spojené s učenou
… více »Český statistický úřad rozšiřuje Statistický geoportál o Datový portál GIS s otevřenými geografickými daty. Ten umožňuje stahování datových sad podle potřeb uživatelů i jejich prohlížení v mapě a přináší nové možnosti v oblasti analýzy a využití statistických dat.
Kevin Lin zkouší využívat chytré brýle Mentra při hraní na piano. Vytváří aplikaci AugmentedChords, pomocí které si do brýlí posílá notový zápis (YouTube). Uvnitř brýlí běží AugmentOS (GitHub), tj. open source operační systém pro chytré brýle.
Jarní konference EurOpen.cz 2025 proběhne 26. až 28. května v Brandýse nad Labem. Věnována je programovacím jazykům, vývoji softwaru a programovacím technikám.
Na čem aktuálně pracují vývojáři GNOME a KDE Plasma? Pravidelný přehled novinek v Týden v GNOME a Týden v KDE Plasma.
Před 25 lety zaplavil celý svět virus ILOVEYOU. Virus se šířil e-mailem, jenž nesl přílohu s názvem I Love You. Příjemci, zvědavému, kdo se do něj zamiloval, pak program spuštěný otevřením přílohy načetl z adresáře e-mailové adresy a na ně pak „milostný vzkaz“ poslal dál. Škody vznikaly jak zahlcením e-mailových serverů, tak i druhou činností viru, kterou bylo přemazání souborů uložených v napadeném počítači.
Byla vydána nová major verze 5.0.0 svobodného multiplatformního nástroje BleachBit (GitHub, Wikipedie) určeného především k efektivnímu čištění disku od nepotřebných souborů.
Jinými slovy, mám-li nějaké klasické použití podmínkové proměnné,
pthread_mutex_lock( &queueMutex ); while ( itemsAvailable == 0 ) { pthread_cond_wait( &queueCondVar, &queueMutex ); } pthread_mutex_unlock( &queueMutex );
má být proměnná itemsAvailable
deklarovaná jako volatile
?
Argument pro: Přistupuje se k ní z několika vláken. (Sice ji v daný okamžik čte vždy jen jedno vlákno, ale to není podstatné.)
Argument proti: Nehrozí, že by kompilátor optimalizoval přístup k proměnné, pokud není lokální a zároveň cyklus obsahuje volání nějaké funkce. To je přesně tento případ.
Zatím jsem pokaždé v těchto situacích volatile
použil, ale tuhle jsem se pohádal s kolegou, který tvrdí, že to je naprosto zbytečné. Co si o tom myslíte vy?
Naprosto zbytecne a jeste bych rekl ze to brani kompilatoru delat optimalizace pristupu k te promenne.
itemsAvailable
modifikovana a citana potencialne z roznych vlakien, musia byt pre spravne fungovanie zabezpecene dve veci: 1) musi byt zabezpecena naslednost akcii a 2) musia byt zmeny sposobene zapisujucim vlaknom viditelne v citajucom vlakne.
Ak su zmeny atomicke, napr.
itemsAvailable = 0;staci na zabezpecenie oboch oznacenie premennje ako
volatile
. Ak su ale zmeny neatomicke, napr.
itemsAvailable++;alebo
itemsAvailable = !itemsAvailable;vtedy musi prist k slovu "tazkotonazna" synchronizacia. Vyssie uvedeny kod synchronizaciu obsahuje; teda aspon pri citani premennej
itemsAvailable
. Dolezite je, aby vsetky zapisy do tejto premennej boli vykonane pod tym istym zamkom. Ak je tomu tak, premenna volatilna byt nemusi, lebo obe vyssie uvedene podmienky zabezpeci synnchronizacia.
Na zaver by som uz len rad dodal, ze akekolvek argumenty typu "bez volatile to bude rychlejsie" alebo "volatile zabrani optimalizaciam" su nezmyselne. Synchronizacia, zamykanie, volatilnost ci akykolvek iny mechanizmus zabezpecenia spravnosti komunikacie asynchronnych udalosti jednoducho nesmie byt obetovany kvoli vykonnosti. Je k nicomu mat rychly a nespravny program. Ak sa ukaze, ze synchronizacia sposobuje superenie o zamok a brani skalovatelnosti, je potrebne zmenit dizaj na skalovatelnejsi, ale nie jednoducho odstranit synchronizaciu, ktora zabezpecuje spravnost programu.
open()
a tusim bolo implementovane ovladacmi, pricom drviva vacsina implementacii ho implementovala ako prazdnu funkciu. Preto sa natiskala myslienka proste ho odstranit. V zapati sa zistilo, ze odstranene nemoze byt, lebo je synchronizovane. Takze hoci volanie nic nerobi per-se, sluzi ako synchronizacna bariera: pocka na ziskanie synchronizacneho zamku a v zapati ho uvolni. Ak by sa toto volanie odstranilo, odstranila by sa synchronizacna bariera a kod by mohol pokracovat skor, ako by mal, t.j. mohol by vidiet udajove struktury v nekonzistentnom stave a inicializacia ovladaca by nemusela prebehnut spravne.
Rovnake je to s volatilnostou premennej: bud tam kvoli spravnosti fungovania programu byt musi, alebo nemusi. Argumenty o optimalizacii su irelevantne.
Samozrejme ze musime zachovat synchronizaci. O tom zadna. Jde jenom o to ze jestli je ta promenna intenzivneji pouzivana uvnitr synchronizace tak to ze by byla volatile by mohlo zpusobit horsi optimalizaci (uvnitr synchronizacniho bloku).
No, popravde jsem pokladal to ze veskery pristup k dane zdilene promenne je synchronizovan za samozrejmost...
Nedá se spoléhat ani na to, že inicializace
itemsAvailable = 0;
je za všech okolností atomická operace. Např.:
int64_t itemsAvailable;
itemsAvailable = 0;
bude na 32-bitovém systému provedena přinejmenším ve dvou instrukcích a proto se nelze na atomicitu inicializace vždy spoléhat.
LOCK
zamyká pouze jednu instrukci a pokud i následující instrukce má rovněž prefix LOCK
, neznamená to, že mezi těmito instrukcemi nemůže nastat přerušení. K zajištění nepřerušitelnosti souvislého úseku kódu slouží privilegovaná instrukce pro maskování přerušení a privilegovaná je z toho důvodu, že odstavení přerušení je potencionálně nebezpečná operace. Proto pochybuju, že neprivilegovaným prefixem LOCK
by šlo dosáhnout stejného efektu, to by nedávalo moc velký smysl.
Záleží na hardwarové platformě. Pokud platforma nepodporuje atomický zápis 64b proměnných do paměti a realizuje ho dvěmi po sobě jdoucími 32b zápisy, tak přiřazení není atomické bez ohledu na to, zda je 64b proměnná deklarována jako volatile či ne.
volatile long
u je atomický. C++ bude mít vícevláknový memory model až v C++1x.
Ja bych na to odpovedel asi takhle: rozhodne pouzit.
Volatile rika kompilatoru, ze promena muze byt modifikovana nejakym "neznamym" zpusobem (HW, jine vlakno, ...) - jedina vec kterou to zpusobi v tomhle pripade bude, ze se promena bude cist vzdy z pameti (nebude se optimalizovat ulozenim do registru).
S argumentem pro souhlasim. Argument proti muze davat smysl, ale spolehat se na to, ze treba na ten kod nepouzije nekdo jinej kompilator (ktery to bude optimalizovat) je nesmyslne (popripade to pouzijete nekde jinde, kde se to bude optimalizovat).
Rekl bych ze se to bude chovat takhle:
1.) s volatile - mate jistotu ze to vzdy fungovat - promena se bude cist vzdy z pameti
2.) bez volatile - bud to kompilator nezoptimalizuja a vysledny kod bude stejny jako v 1.), nebo to kompilator zomptimalizuje a nebude to fungovat
Ja bych na to odpovedel asi takhle: rozhodne pouzit.Nie je celkom pravda. Naco vynucovat zbytocny flush cache (pozri nizsie) po zapise, ak je to uz osetrene lockovanim?
Volatile rika kompilatoru, ze promena muze byt modifikovana nejakym "neznamym" zpusobem (HW, jine vlakno, ...) - jedina vec kterou to zpusobi v tomhle pripade bude, ze se promena bude cist vzdy z pameti (nebude se optimalizovat ulozenim do registru).Nie je celkom pravda. V konecnom dosledku hodnota premennej musi skoncit v registri tak ci tak. Volatilnost sposobi, ze pri zapise premennej (presnejsie pred nim) sa invaliduje jej kopia v cache pamati vsetkych procesorov a (po zapise) sa cache flushne do hlavnej pamate. Zaroven sa tato akcia vykona atomicky, a to i v pripade, ak na zapis celej hodnoty je potrebnych viacero cyklov (niektore udajove typy sa zapisuju "nadvakrat"). Ak bude chciet iny procesor pristupovat k premennej, musi si natiahnut jej aktualizovanu hodnotu z hlavnej pamate.
1.) s volatile - mate jistotu ze to vzdy fungovat - promena se bude cist vzdy z pametiNie je celkom pravda. Bude to fungovat v pripade atomickeho zapisu. V pripade modifikacie typu
itemsAvailable++;
volatilnost nestaci, lebo, modifikacia nie je atomicka, ale sklada sa z troch krokov: 1) nacitania starej hodnoty, 2) inkrementacie a 3) zapisania novej hodnoty. Ak dojde k preruseniu vlakna medzi tymito krokmi a viacero vlakien sa pokusi o to iste, bez synchronizacie to povedie k chybnym vysledkom. Volatilnost premennej staci iba ak nova hodnota priradena do premennej je nezavisla na predchadzajucej hodnote. V opacnom pripade musi byt synchronizovany cely blok vykonavajuci citanie starej a vypocet a priradenie novej hodnoty.
Pokud se nová hodnota počítá ze starší, dá se v jistých případech zamykání vyhnout použitím instrukce CAS (en.wikipedia.org/wiki/Compare-and-swap).
pthread_mutex_lock
) rozhodně nemá a ani nemůže mít vliv na synchronizovanou proměnnou, protože vůbec netuší, kterou proměnnou nebo proměnné daný synchronizační objekt (mutex) vlastně synchronizuje. Volání pthread_mutex_lock(&mutex)
tak nanejvýš vyprázdní, či jinak synchronizuje, cache přidělenou objektu mutex
, ale víc udělat nedokáže, nemá k tomu žádné informace.
volatile
v C/C++ neříká překladači nic o tom, jak se k proměnné chovat při zápisu. Pouze sděluje, že mezi dvěma čteními může dojít ke změně a tudíž nelze hodnotu takové proměnné uchovávat v registrech procesoru, ale vždy je nutné ji načíst znovu z paměti. Atomické zamykání instrukcí a podobné techniky lze realizovat pouze na úrovni assembleru, jinak je vynutit v C/C++ nejde.
Presne tak. Promena typu "volatile" nerika vubec nic o vyprazdneni cache. Je rozhodne nutna pro specifikaci, ze k promene pristupuje vice vlaken - tudiz nelze si uchovavat "mezikroky" treba v registrech.
Pr;:
$ cat a.c
int main(){
volatile int i = 0;
i++;
return i;
}
$ gcc -S -O3 a.c && cat a.s
.file "a.c"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $16, %esp
movl $0, -8(%ebp)
movl -8(%ebp), %eax
addl $1, %eax
movl %eax, -8(%ebp)
movl -8(%ebp), %eax
addl $16, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.1.2 20070626 (Red Hat 4.1.2-14)"
.section .note.GNU-stack,"",@progbits
$ cat a2.c
int main(){
int i = 0;
i++;
return i;
}
$ gcc -S -O3 a2.c && cat a2.s
.file "a2.c"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
movl $1, %eax
pushl %ebp
movl %esp, %ebp
pushl %ecx
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.1.2 20070626 (Red Hat 4.1.2-14)"
.section .note.GNU-stack,"",@progbits
Tady je pekne videt, ze "volatile" ma vliv jen na ulozeni mezi-vysledku (dokonce pokud neni promena deklarovana jako volatile, tak nemusi byt v pameti ani alokovana a je platna jen pro aktualni vlakno). Zadna prace s cache.
To se nam tu urodilo nesmyslu. V prvni rade bych zcela ignoroval prispevky kolegy cronina, protoze ty se mozna tykaji javy (ale tu neznam, takze nedokazu posoudit, jestli to co pise je pravda alespon v ni), ale urcite ne c/c++.
Ve vasem pripade je volatile
zbytecne, protoze promennou itemsAvailable
mate chranenou zamkem queueMutex
a tedy k ni muze v jednom okamziku pristupovat pouze jedno vlakno. Zaroven fce pthread_mutex_*
funguji jako pametove bariery, nemuze se stat, ze by vam pres ne "pretekaly" cteni ci zapisy.
Klicove slovo volatile
je pro synchronizaci mezi vlakny nepouzitelne, protoze jim sice ukecate prekladac, ale procesor muze zapisy do pameti prerovnavat z vlastniho rozmaru.
Klíčové slovo volatile
rozhodně zbytečné v uvedeném příkladě není, mutex pouze zabraňuje současnému přístupu z více vláken najednou, ale nedokáže zabránit překladači, aby vygeneroval kód, kdy si před vstupem do cyklu uloží obsah proměnné itemsAvailable
do registru a následně už testuje pouze obsah tohoto registru, aniž by si pokaždé znovu načetl aktuální hodnotu itemsAvailable
.
Jak synchronizace mutexem, tak klíčové slovo volatile
jsou nezbytně nutné!!!
Mylite se, mutex, respektive volani tech tri funkci, presne tomuto zabrani, volatile
je tam naprosto zbytecne.
Možná na vysvětlenou ještě malá poznámka, volání funkce pthread_cond_wait(&cond, &mutex)
uvolní mutex
a čeká, až nějaké jiné vlákno zavolá pthread_cond_signal(&cond)
, čímž se cond
probudí z waitu a poté mutex
opět zamkne. Pokud by tam nebylo ono pthread_cond_wait(&cond, &mutex)
, potom by samozřejmě k žádné změně proměnné itemsAvailable
dojít nemohlo (za předpokladu, že ostatní vlákna zodpovědně synchronizují přístup k itemsAvailable
přes stejný mutex
) a jednalo by se o nekonečnou smyčku.
Doufám, že se nám to konečně podařilo společným úsilím rozmotat.
volatile
z Javy na C bola nespravna. Medizcasom som si na zaklade tejto diskusie nastudoval detaily o C - skutocne tam volatile
negarantuje - narozdiel od Javy, kde su garancie tohto klucoveho slova dost silne a zahrnaju aj sematiku happen before - takmer nic.
Stale vsak pochybujem o tomto:
Ve vasem pripade je volatile zbytecne, protoze promennou itemsAvailable mate chranenou zamkem queueMutex a tedy k ni muze v jednom okamziku pristupovat pouze jedno vlakno. Zaroven fce pthread_mutex_* funguji jako pametove bariery, nemuze se stat, ze by vam pres ne "pretekaly" cteni ci zapisy.Zmieneny mutex tak ako ho vidime zabezpeci, ze ten konkretny blok kodu bude vykonavany iba jednym vlaknom. Nijako to ale nezabrani tomu, aby ine bloky kodu pracujuce s inkriminovanou premennou bezali pararelne, ci uz preto, ze nie su synchronizovane vobec, alebo preto, ze su synchronizovane inym zamkom. Alebo sa opat mylim a zapis do premennej nemusi byt synchronizovany pomocou toho isteho mutexu ako citanie?
Nijako to ale nezabrani tomu, aby ine bloky kodu pracujuce s inkriminovanou premennou bezali pararelne, ci uz preto, ze nie su synchronizovane vobec, alebo preto, ze su synchronizovane inym zamkom.
To by ale byla chyba programu (stejně jako když mutex nepoužijete vůbec) a proti ní by vám volatile
stejně nijak nepomohlo.
volatile
dobre. Podla toho, o com ma tu presviedcate, je synchronizacia mutexami nutna a postacujuca podmienka pre spravne fungovanie. Kedy mi teda volatile
pomoze?
Vyznam to ma jenom zabranit "nechtene" optimalizaci - vzdy pri dotazu na promenou se podiva do pameti (a ne treba do pracovniho registru kde je ulozena hodnota z posledniho precteni - ona se totiz mohla mezitim prepsat nekym "jinym")
Jsou dve udalosti kde by se to melo pouzit:
1.) Vicevlaknova aplikace a pristup ke globalni promene - hodnotu moze prepsat jine vlakno
2.) Pri komunikaci s HW zarizeni, jez je mapovano nekam do pameti (na presnou znamou adresu) - vzdy je nutne cist pametove misto, jelikoz HW to muze kdykoli zmenit
Snad se mi to konecne podarilo rozumne vysvetlit - pri vypnute optimalizaci to smysl nema, jelikoz se nepouzivaji registry pro uchovavani mezivysledku.
No a prave u bodu 1) je volatile
zbytecne, od toho mate zamky.
Zamky vam v tomhle vubec nepomuzou - zamky vam zaruci na jednom miste budete provade jen jednu operaci, ane treba ze zaroven bude cist a zoroven zapisovat (coz muze vyprodukovat uplne jina data nez ktera byla predtim, nebo potom).
Tohle jen rika prekladaci, ze "ta hodnota se muze zmenit".
Teoreticka situace: promenou itemsAvailable nebudete deklarovat jako volatile - chytry prekladac si vsimne, ze v tom while cyklu se nikde nemeni (a zjisti, ze ani volani pthread_cond_wait ji zmenit nemuze) a tak si rekne ze hodnotu ulozi do registru (aby neprovadel drahe opearace cteni pameti) - takze muzou nastat dve situace - pred cylkem bude itemsAvailable == 0, pak vznikne nekonecny cyklus (protoze se stale bude porovnavat 0 == 0 bez ohledu na to zda itemsAvailable nekdo zmenil ci ne, protoze mame jeji "kopii" v registru), nebo itemsAvailable != 0 a cylkus vubec neprobehne.
Pro názornost:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; volatile int x = 0; // 1. vlákno pthread_mutex_lock(&mutex); // zamkne mutex while (x == 0) // při každém průchodu čtu novou hodnotu x (mohlo ji modifikovat jiné vlákno) { pthread_cond_wait(&cond, &mutex); // odemkne mutex (umožní tak modifikaci x), čeká na pthread_cond_signal, zamkne mutex } pthread_mutex_unlock(&mutex); // odemkne mutex // 2. vlákno pthread_mutex_lock(&mutex); // zamkne mutex x = 1 pthread_cond_signal(&cond); // způsobí probuzení pthread_cond_wait v 1. vlákně pthread_mutex_unlock(&mutex); // odemkne mutex a umožní tak 1. vláknu si ho opět přivlastnit ve funkci pthread_cond_wait
Pokud by proměnná x
nebyla volatile
, měl by překladač plné právo předpokádat, že hodnota x
se nemůže sama od sebe změnit a může ji tedy beztrestně uložit do registru. Napadlo mě jen, že díky tomu, že v cyklu je ještě obsaženo volání funkce pthread_cond_wait
o níž neví, zda nemodifikuje obsah x
, tak se překladač nemůže spoléhat, že x
zůstane zachované a žádnou optimalizaci ohledně x
si nemůže dovolit. Avšak v případě, že pthread_cond_wait
je inline funkce a nevolá žádné další non-inline funkce, mohl by kompilátor vědět, zda se hodnota x mění, či ne a naznačenou optimalizaci by mohl provést. Přenositelný kód by neměl dělat žádné předpoklady o funkci pthread_cond_wait
(inline/non-inline), a proto je užití volatile
jistě opodstatněné, i když s velkou pravděpodobností jeho absence nezpůsobí žádné problémy.
Přesně takhle jsem uvažoval taky a zatím jsem volatile
vždy používal. Nejspíš tím nemůžu nic zkazit, protože jakákoliv případná optimalizace přístupu k té proměnné by v každém případě byla nekorektní. Takže v podstatě o žádnou důležitou (a proveditelnou) optimalizaci nepřicházím. Dokonce se mi občas zdá, že volatile
může i trochu zlepšit přehlednost a čitelnost toho kódu, protože jasně říká bacha, tímhle kouskem dat se něco synchronizuje.
Napadlo mě jen, že díky tomu, že v cyklu je ještě obsaženo volání funkcepthread_cond_wait
o níž neví, zda nemodifikuje obsahx
, tak se překladač nemůže spoléhat, žex
zůstane zachované a žádnou optimalizaci ohledněx
si nemůže dovolit.
Tak s tímto souvisí hlavní důvod, proč se pořád přikláním k názoru, že by tam volatile
mělo být. Otázka totiž je, který překladač si tu optimalizaci nedovolí. Jistě, současný překladač GCC to skutečně neumí a když je v cyklu volání funkce, všechny nelokální proměnné vždy čte z paměti. Nicméně nepřipadá mi správné se na takové chování i do budoucna spoléhat.
Člověk si musí položit otázky: Co když přijde úžasny-překladač-z-budoucnosti (tm), který bude umět analýzu aliasů i přes volání funkcí? A co když někoho někdy napadne použít můj kód v něčem, co se linkuje staticky? Tam by možná agresivnější analýzu aliasů zvládl i některý dnešní překladač, který není jen výplodem mé fantazie...
Linkování je až následná operace po kompilaci a během linkování již k žádným optimalizacím dojít nemůže, pouze se propojují odkazy na proměnné a funkce, měnit kód se už v této fázi nedá.
Tohle ani dnes není tak úplně pravda. Navíc je otázka, jako to bude v budoucnu.
Kdybych měl celou diskusi vtěsnat do jedné otázky: Garantuje mi samotný standard C++ (nebo C), že se nikdy nestane, aby neaktuální verze (non-volatile) proměnné zůstávala v registru i přes volání funkcí?
Je úplně jedno, že nějaký konkrétní překladač zrovna dnes takové věci nedělá. To je chabá útěcha. Předpoklady na chování toho kódu musí mít oporu v nějakém standardu.
Garantuje mi samotný standard C++ (nebo C), že se nikdy nestane, aby neaktuální verze (non-volatile) proměnné zůstávala v registru i přes volání funkcí?
Ano. Objekt musi byt v pameti na jednom miste, jestlize prekladac provadi takove optimalizace, diky kterym je de facto na dvou ruznych mistech (v registru a v RAMce), musi se postarat o to, aby to navenek nebylo poznat.
Pokud jeji adresu nedate do globalni promenne, nepredate ji nejake funkci, nebo s ni neudelate nejakou jinou skopicinu, tak ji zbytek programu nemuze videt.
Frknu to sem k vam, ale tyka se to i ostatnich volatilistu.
Pratele, mne se zda, ze nejen ze neznate standard, ale ani se na chvili nezamyslite nad tim, kde bychom s vasimi predstavami dovedenymi do dusledku skoncili. Prekladac muze optimalizovat kod, ale obecne jen do te miry, aby nezmenil pozorovatelne chovani programu. Standard predpoklada, ze vsechny objekty lezi kdesi v pameti, jedne pro cely program, a prekladac muze optimalizovat pristup k nim tim, ze si je natahne do registru procesoru, nicmene toto muze udelat pouze tehdy, pokud je presvedceny, ze je nikdo mezitim v pameti neprepsal. Vezmneme si trivialni priklad:
void clobber(); bool test(int* a) { *a = 3; clobber(); if( *a == 3 ) return true; else return false; }
Po zavolani fce clobber()
musi prekladac znovu nacist hodnotu *a
z pameti, nemuze vedet, jestli se mezitim nezmenila, protoze nezna definici clobber()
a nevi, co presne dela. Takze zaverecny if
nemuze odoptimalizovat.
Tohle plati obecne pro vsechny funkce a pro pthread_mutex_lock
, pthread_mutex_unlock
a pthread_cond_wait
obzvlast, nebot za jejich "behu" muze dojit ke zmene v pameti uz z definice. Nejaky hypoteticky prekladac budoucnosti by teoreticky tuto optimalizaci mohl provest, ale jen pokud by zanalyzoval cely program a dosel k zaveru, ze zadne jine vlakno a ani nikdo jiny nebude v prubehu vykonavani fce pthread_cond_wait()
menit hodnotu itemsAvailable
. Dokud si timto nebude jist, tak vzdy bude muset znovu natahnout hodnotu z pameti, jinak se nechova podle standardu.
Abych to shrnul a asi po treti zopakoval sam sabe. Klicove slovo volatile
je zcela zbytecne, protoze to oc vam jde, totiz natahnuti hodnoty promene z pameti po pripadnem probuzeni, zaruci samo volani funkci kolem mutexu. Deklaraci volatile
akorat svuj program zpomalujete (potencialne docela hodne, zalezi jak casto s takovou promennou pracujete), protoze prekladac nutite nacitat/zapisovat promennou z/do pameti vzdy, i kdyz nehrozi, ze by ji nekdo jiny zmenil.
Pochybuju o tom, že ve standardu je něco o tom, že překladač musí předpokládat, že kterékoli volání funkce může změnit hodnotu proměnné v paměti – daleko pravděpodobnější se mi jeví, že může dělat analýzu inline funkcí a zjistit, že volání funkce nemůže hodnotu proměnné změnit a může ji tak optimalizovat.Obzvlášť v uvedeném příkladu, kdy funkci clobber() není hodnota lokální proměnné a nijak předávána, takže se dá docela dobře předpokládat, že tam se a nemění. A i kdyby - jak už psali jiní, obecně je pitomost spoléhat se na chování překladače, protože to se může s jinou verzí změnit.
To je právě takové dilema.
Agresivní přístup: Na zásobník mi nemá kdo co sahat bez dovolení! Když mi tam sáhne, dostane do nosu a je to jeho problém. Pokud nikomu nepředám pointer na svou lokální proměnnou, neexistuje žádný korektní způsob, jak by mohl proměnnou změnit. Tedy mohu optimalizovat.
Ultrakonzervativní přístup: Kdykoliv předám řízení někomu jínému (tj. zavolám funkci), musím počítat s tím, že volaná funkce může přepsat kterékoliv místo v paměti. Neřeším, zda je takový zápis korektní nebo ne. Každou proměnnou (včetně lokální) musím po volání funkce znovu načíst.
Otázka je, co o tom říká standard a jak přesně to překladače řeší. Já bych tipoval, že pravda jen někde uprostřed, nicméně podle některých příspěvků by měl standard vynucovat spíš ten konzervativní přístup...
Nikolivek. Stale plati pravidla o viditelnosti, takze pokud mam lokalni promennou, kterou nikde neinzeruju (treba ze bych nejake funkci predal odkaz na ni), podle standardu se k ni ostatni casti programu nemaji sanci dostat a proto si napr. prekladac muze dovolit ten luxus predpokladat, ze takova promenna se mu nebude menit pod rukama.
Po zavolani fce clobber() musi prekladac znovu nacist hodnotu *a z pameti, nemuze vedet, jestli se mezitim nezmenila, protoze nezna definici clobber() a nevi, co presne dela. Takze zaverecny if nemuze odoptimalizovat.
Vsechny parametry vcetne odkazu jsou predavane hodnotou a stavaji se lokalnimi promennymi volane funkce, volajici funkce ani nikdo jiny se k nim po predani kontroly nad behem programu nemuze dostat. Samozrejme volana funkce se muze o sve promenne podelit z vlastni vule, ale pokud nekomu dalsimu preda odkaz na parametr, ktery byl predan v registru, tak prekladac musi vygenerovat kod, ktery udela misto v pameti (nejspis na zasobniku), obsah regitru tam nakopiruje a pote preda ukazatel. To uz ale neni starost volajici funkce.
Pokud tohle volajici porusi, tak je to jeho chyba a s tim cecko tezko neco udela. To byste taky mohl klast C za vinu, ze nebude fungovat, kdyz mu budete paremetry funkci sypat na zasobnik v opacnem poradi, nez ocekava.
Předpokládám, že jste chtěl napsat adresa lokální proměnné.To předpokládáš špatně, chtěl jsem napsat lokální proměnná. To že je to náhodou pointer, je totiž úplně jedno. (Když clobber() nezná pointer, nemůže se rejpat ani v hodnotě)
A proc by clobber()
ten pointer nemohl znat? Tady mate clobber()
implementovan tak, ze se k promenne *a
dostane a zmeni ji, takze test
vrati false
.
Tady mate clobber() implementovan tak, ze se k promenne *a dostane a zmeni ji, takze test vrati false.Kdežto tady, což je příspěvek, ke kterému jsem se vyjadřoval, ne.
Kdybyste cetl pozorne, tak byste si vsiml, ze v tom prispevku vubec neni clobber()
definovana. Takze mi neni jasne, na zaklade ceho jste jen tak od boku vystrelil, ze se v hodnote rejpat nemuze. A ze to mozne je, dokazuje druhy kus kodu.
Takze mi neni jasne, na zaklade ceho jste jen tak od boku vystrelil, ze se v hodnote rejpat nemuze.Na základě tohohle:
Obzvlášť v uvedeném příkladu, kdy funkci clobber() není hodnota lokální proměnné a nijak předávána, takže se dá docela dobře předpokládat, že tam se a nemění.Tím samozřejmě neříkám, že nemůže. Ale zavání to nějakou prasárnou.
Pokud clobber() má okopírovanou hodnotu lokální proměnné, může si s tou hodnotou dělat co chce, ale původní proměnnou to nezmění.To sice ne, ale vzhledem k tomu, že ta hodnota je shodou okolností pointer, tak funkce sice nezmění tu původní lokální proměnnou, ale data v ní (potenciálně viditelná jinde) už jo.
Vsimnete si, ze promenna *a
neni lokalni, lokalni je jen odkaz na ni, predany jako parametr.
Pokud ten kod nahore ulozime jako a.cc, nasledujici kus jako b.cc a obvyklym zpusobem je zkompilujeme, jakou myslite, ze program vrati hodnotu? Bude zaviset na nastavenych optimalizacich?
bool test(int* a); int a; void clobber() { a = 10; } int main(int argc, char** argv) { return test(&a); }
Spíš bych řekl, že překladač může použít optimalizaci pokud ví, že to samé vlákno proměnnou v paměti nezmění.
Ano, a protoze c/c++ standard se o vlaknech vubec nezminuje, prekladac se chova vzdy tak, jako by bezelo jen jedno vlakno.
Znovu opakuji, pro standard existuje jenom jedna pamet do ktere sahaji vsichni, takze pokud nejaka funkce zmeni spolecnou promenou, ostatni tu zmenu nemuzou neprosvihnou. Prace s registry je problem prekladace a on musi zajistit, ze tomu tak bude i s optimalizovanym kodem. Proto napr. v gcc muzete funkci deklarovat s atributem const (mysleno takove to __attributte__ ((const))
), cimz prekladaci date najevo, ze funkce zavisi pouze na svych parametrech, necte ani nemeni zadne globalni promenne, tudiz behem jejiho volani neni nutne soupat s "ohrozenymi" promennymi z registru do pameti a zase zpatky.
Pratele, mne se zda, ze nejen ze neznate standard, ...
V mém případě je to naprostá pravda. Tu šílenou bichli jsem skutečně nečetl.
Po zavolani fceclobber()
musi prekladac znovu nacist hodnotu*a
z pameti, nemuze vedet, jestli se mezitim nezmenila, protoze nezna definiciclobber()
a nevi, co presne dela. Takze zaverecnyif
nemuze odoptimalizovat.
Vsuktu? Platí tohle i v případě, že knihovnu s funkcí clobber()
linkuji staticky? Co když funkce clobber()
neobsahuje vůbec žádný mov se zápisem do paměti (na Intelu)? Co když neobsahuje žádný store (na nějakém RISCu)? Klidně může existovat funkce, která vůbec nikam do paměti nepíše. (Kromě manipulace se zásobníkem, ale na tu opravdu nikdo jiný paralelně nesahá.) Opravdu si něčeho takového kompilátor nesmí všímat? Ani v budoucnu?
Nejaky hypoteticky prekladac budoucnosti by teoreticky tuto optimalizaci mohl provest, ale jen pokud by zanalyzoval cely program a dosel k zaveru, ze zadne jine vlakno a ani nikdo jiny nebude v prubehu vykonavani fcepthread_cond_wait()
menit hodnotuitemsAvailable
.
Žádný překladač se určitě nepustí do tohoto algoritmicky neřešitelného problému. Nicméně pořád mi připadá v zásadě správné tu proměnnou označit jako
volatile
...
Klicove slovo volatile
je zcela zbytecne, protoze to oc vam jde, totiz natahnuti hodnoty promene z pameti po pripadnem probuzeni, zaruci samo volani funkci kolem mutexu.
Možná. Například klíčové slovo const
před deklarací pointerů je taktéž na spoustě míst (na první pohled) zbytečné, nicméně chrání programátora před budoucími ošklivými chybami. Říká „pozor, s těmito daty si nemůžeš hrát, ta ještě bude někdo potřebovat“. A volatile
zase říká „pozor, s těmihle daty si občas paralelně hraje někdo jiný“. Že kompilátor možný problém postřehne a že nebude optimalizovat přístup do paměti přes volání funkcí, není z pohledu logiky celé věci až tak relevantní.
Deklaraci volatile
akorat svuj program zpomalujete (potencialne docela hodne, zalezi jak casto s takovou promennou pracujete), protoze prekladac nutite nacitat/zapisovat promennou z/do pameti vzdy, i kdyz nehrozi, ze by ji nekdo jiny zmenil.
Zpomaluji? Pokud se ta proměnná čte i zapisuje vždy pouze pod (jedním a tím samým) mutexem a pokud mezi každými dvěma přísupy k té proměnné leží alespoň jedno volání funkce (třeba pthread_cond_wait()
, když nic jiného), měla by být „rychlost“ programu s volatile
i bez volatile
srovnatelná. Nebo jsem něco přehlédl?
Velmi striktně vzato, jediné místo, kde je volatile
skutečně bezpodmínečně potřeba, je spinlock. (Ten samozřejmě v userspace nemá co dělat, leda snad v implicitní podobě, kterou zajišťuje adaptivní mutex.) Nicméně stále mám (možná mylný) dojem, že použití volatile
u podmínkové proměnné je tak nějak „logicky správné“. Navíc (jak už jsem poznamenal) skutečně nevidím žádné nevýhody stran efektivity. (Ale můžu se mýlit, což se mi stává často.)
volatile
nemá z pohledu thread-safety žádný efekt. Asi nejlíp to vysvětluje Arch Robinson, architekt intelích Threading Building Blocks: Volatile: Almost Useless for Multi-Threaded Programming (včetně diskuse).
Já teda nejsem Céčkař, ale pod vlivem téhle diskuse jsem dneska chvíli trápil Google a v zásadě se věc má tak, že Céčkové volatile nemá z pohledu thread-safety žádný efekt.Kdyby sis diskuzi pročetl pořádně, zjistil bys, že to skoro nikdo netvrdí.
volatile
je v předmětném případě dobré, někdo, že je dokonce nezbytné, volatilisti by si tu s nevolatilistama nejradši vyrvali vlasy To to vazne vysvetluju tak blbe?
Muzete si to predstavit tak, ze na zacatku je vsude takove male volatile, ktere optimalizator postupne vyskrtava. Kdyz si vezmete treba takovyhle kousek kodu:
a = 1; a *= 5; a = a - 3;
a prelozite ho zcela bez optimalizaci, vyplivne vam gcc toto:
movl $1, -8(%ebp) movl -8(%ebp), %edx movl %edx, %eax sall $2, %eax addl %edx, %eax movl %eax, -8(%ebp) subl $3, -8(%ebp)
Jak vidite, bud pracuje primo s pameti, nebo nahraje promennou do registru, provede operaci a zase ji do pameti ulozi.
Pokud budete linkovat staticky, nebo nejakym jinym zpusobem umoznite prekladaci dostat se k definici fce clobber()
, tak jestlize bude schopen vydedukovat, ze clobber()
nemuze *a
zadnym zpusobem zmenit, pak tu optimalizaci muze provest. Ale az pote, co se o tom ujisti, nikdy ne drive.
Pouzitim volatile tyto optimalizace blokujete, pokud a
z prikladu na zacatku deklarujete jako volatile, tak prekladec ten vypocet vzdy preklopi do toho stejneho assembleru, a to i kdyz pristup k a
chranite mutexem a nikdo jiny vam ho nebude menit pod rukama. Nahrani a
do registru po zamknuti mutexu a jeho zapsani do pameti pred uvolnenim mate zaruceno tak jako tak, takze opravdu jenom zpomalujete program, k nicemu jinemu to dobre neni.
Avšak v případě, že pthread_cond_wait je inline funkce a nevolá žádné další non-inline funkce, mohl by kompilátor vědět, zda se hodnota x mění, či ne a naznačenou optimalizaci by mohl provést.
Nemohl. I pokud by thread_cond_wait
byla inline, musi byt nadefinovana tak, aby prekladac vedel, ze se v pameti muze cokoliv zmenit.
Přenositelný kód by neměl dělat žádné předpoklady o funkci pthread_cond_wait (inline/non-inline), a proto je užití volatile jistě opodstatněné, i když s velkou pravděpodobností jeho absence nezpůsobí žádné problémy.
Jenze vychozi stav je, ze volani funkce muze zmenit cokoliv a sahat kamkoliv, takze pokud o ni nedelam vubec zadne predpoklady, musim pred jejim zavolanim vsechny zmeny ulozit do promennych v pameti a po navratu je zase nacist.
Ještě mě napadlo jedno řešení a sice podmínkovou proměnnou nedeklarovat jako volatile
, ale použít přetypování pouze v místě, kde bezpečně víme, že žádnou optimalizaci nechceme připustit.
int x = 0; while (*(const volatile int*)(&x) == 0) // C zápis while (const_cast< const volatile int& >(x) == 0) // C++ zápis
Tak můžeme sdělit překladači, že právě jen v podmínce cyklu se má zdržet jakýchkoliv optimalizací x
a všude jinde nechť přístup k x
optimalizuje dle libosti.
No, mně se zdá, že proměnná, která smí mít někde volatile
a jinde zase non-volatile
přístup, zkrátka zavání chybou v návrhu...
Děkuji všem za komentáře a zejména za odkazy na zajímavé články. Nicméně právě teď jsem udělal praktický pokus s ošklivými spinlocky. Výsledek mě vyděsil. Posuďte sami. Většina toho, co tu zatím bylo řečeno, zcela zjevně není pravda.
Tady je zdrojový kód:
#include <unistd.h> #include <limits.h> #include <stdio.h> #include <pthread.h> #include <time.h> static const struct timespec snooze = { 5, 0 }; static int useless; static int flag = 1; static volatile int FLAG = 1; static void foo( void ) {} static void FOO( int * something ) { useless = *something; } static void * t1( void * param ) { while ( flag ); puts( "T1 finished." ); return NULL; } static void * t2( void * param ) { while ( flag ) foo(); puts( "T2 finished." ); return NULL; } static void * t3( void * param ) { while ( flag ) FOO( &flag ); puts( "T3 finished." ); return NULL; } static void * t4( void * param ) { while ( FLAG ); puts( "T4 finished." ); return NULL; } static void * t5( void * param ) { while ( FLAG ) foo(); puts( "T5 finished." ); return NULL; } void * ( * const func[ 5 ] )( void * ) = { t1, t2, t3, t4, t5 }; int main( void ) { int i; pthread_attr_t attr; pthread_t threads[ 5 ]; pthread_attr_init( &attr ); pthread_attr_setstacksize( &attr, PTHREAD_STACK_MIN ); puts( "Spawning threads." ); for ( i = 0; i < 5; ++i ) pthread_create( &threads[ i ], &attr, func[ i ], NULL ); nanosleep( &snooze, NULL ); puts( "Setting the death flag." ); FLAG = flag = 0; nanosleep( &snooze, NULL ); puts( "That's the end." ); return 0; }
Ano, je to moc ošklivý kód. Ano, nevolám nikde pthread_join()
. (To jsou reakce na předpokládané FAQ.) A tady jsou výsledky, ze kterých mrazí v zádech.
[andrej@argos pokusy]$ gcc -O0 -pthread spin.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. Setting the death flag. T4 finished. T2 finished. T1 finished. T5 finished. T3 finished. That's the end.
Tak tady je vše podle předpokladů. Při -O0
se proměnná čte vždy znovu z paměti.
[andrej@argos pokusy]$ gcc -O1 -pthread spin.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. Setting the death flag. T5 finished. T4 finished. That's the end.
Tady se čtou pouze volatile proměnné. Druhé a třetí vlákno proměnnou flag nepřečtou znovu, přestože se pointer na ni předává ven a přestože se v cyklu volá funkce! Při -O2
a -O3
se to chová stejně. Co když teď zdrojový kód trochu pozněníme...?
--- spin.c 2009-06-18 23:46:52.000000000 +0200 +++ spin2.c 2009-06-18 23:51:55.000000000 +0200 @@ -11,7 +11,7 @@ static volatile int FLAG = 1; static void foo( void ) {} -static void FOO( int * something ) { useless = *something; } +static void FOO( int * something ) { ++( *something ); } static void * t1( void * param ) { while ( flag ); puts( "T1 finished." ); return NULL; } static void * t2( void * param ) { while ( flag ) foo(); puts( "T2 finished." ); return NULL; }
Teď jde opravdu do tuhého a nastává zmatek, jaký jsem ještě neviděl.
[andrej@argos pokusy]$ gcc -O0 -pthread spin2.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. Setting the death flag. T4 finished. T5 finished. T2 finished. T1 finished. That's the end.
Naprosto špatně. Vlákno T3 by mělo skončit první, ale neskončí vůbec. Proměnnou v cyklu nečte i přesto, že je vypnutá optimalizace a že ji předává jiné funkci, která ji pozmění! Dobře, zkusme optimalizaci zapnout.
[andrej@argos pokusy]$ gcc -O1 -pthread spin2.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. T3 finished. Setting the death flag. T4 finished. T5 finished. That's the end.
Ano, takhle to má podle předpokladů (ne)fungovat... O stupeň vyšší optimalizace se chová stejně. Ale u -O3
číhá ošklivé překvapení:
[andrej@argos pokusy]$ gcc -O3 -pthread spin2.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. T3 finished. T1 finished. Setting the death flag. T4 finished. T5 finished. That's the end.
Tohle už je opravdu těžko vysvětlitelné. Teď znovu vyzkouším první zdrojový kód, jen s jiným kompilátorem:
[andrej@argos pokusy]$ icc -O0 -pthread spin.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. Setting the death flag. T4 finished. T5 finished. T3 finished. T2 finished. T1 finished. That's the end.
Tady žádné překvapení nečíhá.
[andrej@argos pokusy]$ icc -O1 -pthread spin.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. T1 finished. T2 finished. T3 finished. Setting the death flag. T5 finished. T4 finished. That's the end.
Ale toto je prosím pěkně neuvěřitelné. Kompilátor přesunul přiřazení globální proměnné přes několik volání funkcí! Netvrdil tu někdo před chvílí, že to není možné? Jedině volatile proměnná byla přiřazena (a přečtena) ve správnou dobu. Vyšší stupně optimalizace se chovají stejně.
Nyní znovu vyzkouším pozměněný zdroják s kompilátorem Intel.
[andrej@argos pokusy]$ icc -O0 -pthread spin2.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. Setting the death flag. T2 finished. T1 finished. T4 finished. T5 finished. T3 finished. That's the end.
Toto je další odlišnost od GCC. Tentokrát skončila všechna vlákna. Nicméně vyšší úrovně optimalizace přinesou další překvapení:
[andrej@argos pokusy]$ icc -O1 -pthread spin2.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. T2 finished. T1 finished. Setting the death flag. T5 finished. T4 finished. That's the end.
Další podivný výsledek. Jedno z vláken neskončilo a tento stav se nepodobá žádnému z předchozích.
[andrej@argos pokusy]$ icc -O2 -pthread spin2.c -o spin [andrej@argos pokusy]$ ./spin Spawning threads. T1 finished. T2 finished. T3 finished. Setting the death flag. T4 finished. T5 finished. That's the end.
A zvýšení stupně optimalizace to zase dá do pořádku, přestože jde o jiný kompilátor a jiný stupeň optimalizace. Třetí stupeň se chová stejně.
Jaké je ponaučení z tohoto pokusu?
Přes volání statických funkcí optimalizuje kompilátor skoro vždy a nelze tomu zabránít.
Schopnost gcc
zjistit, zda je volané funkci předáván pointer na danou proměnnou a zda ji tato volaná funkce může nebo nemůže změnit, zjevně závisí na zvolené úrovni optimalizace. (!!!) Výsledky z GCC po aplikaci patche to jasně ukazují. Tohle se vůbec netýká vláken a mohl by to být bug v kompilátoru.
A jeden fakt na závěr: Vlákna čtoucí volatile proměnnou neselhala ani jednou. Ostatní selhala téměř vždy, přestože podle většiny odpovědí v této diskusi by selhat neměla!
Jestliže kompilátor dokáže přesouvat přiřazení globální proměnné přes několik systémových volání, přes vytváření vláken a dokonce i přes nanosleep()
, jak má potom člověk věřit, že je nebude přesouvat přes synchronizační primitiva?
Když může přiřazení do globální proměnné přeskočit nanosleep()
, proč by nemohlo přeskočit pthread_cond_wait()
nebo pthread_cond_signal()
? Jak je možné, že vůbec nějaká synchronizace funguje? Možná mi budete spílat, ale pro mě je závěr z tohoto pokusu jednoznačný: volatile
vždy a všude!
Tak teď jsem z toho jelen.
Je zjevné, že kompilátor Intel někdy přesouvá přiřazení globální proměnné směrem nahoru i přes několik volání funkcí.
Nicméně GCC jsem hodně křivdil a byla to moje chyba. V tom pozměněném zdrojáku je inkrementace té proměnné. Tedy jde o naprosto normální race condition, která může dopadnout pokaždé jinak a samozřejmě závisí na zvolené optimalizaci. Tedy za humbuk kolem vlákna T3 se moc omlouvám. Tak to dopadá, když člověk dělá zoufalé pokusy pozdě v noci.
Je možné, ne-li pravděpodobné, že GCC přiřazení do proměnné flag
buď přesune směrem dolů, nebo zcela vynechá, protože flag
už funkce main()
po přiřazení nikde nečte. FLAG
je volatile, tedy se vždy správně přiřadí.
Podivné chování vláken při některých optimalizacích se ovšem nedá vysvětlit jinak než tím, že kompilátor přesouvá přiřazení globální proměnné přes několik volání funkcí. Fakt je tohle korektní? Kde mám vzít jistotu, že to přiřazení nepřesune přes pthread_mutex_lock()
a pthread_mutex_unlock()
???
Některé výsledky mého pokusu možná přece jen budou mít racionální vysvětlení a už mě to tolik neděsí. Nicméně pořád jsem z toho dost (nepříjemně) překvapený. Svět prostě nefunguje tak, jak jsem si dlouhou dobu představoval. Že jsem zatím při programování na žádnou z těchto ošklivých situací nenarazil, to je prostě obrovská šťastná náhoda. Jinak si to neumím vysvětlit.
Například provedu-li tohle,
pthread_mutex_lock( &queueMutex ); ++itemsAvailable; pthread_cond_signal( &queueCond ); pthread_mutex_unlock( &queueMutex );
kde vezmu jistotu, že kompilátor neprovede inkrementaci někdy před pthread_mutex_lock()
nebo po pthread_mutex_unlock()
? Obojí by mělo fatální následky. Čím přesně je zaručeno, že se to nestane? Jinými slovy, čím přesně se ty dvě zmíněné funkce liší od nanosleep()
, pthread_ceate()
, puts()
a dalších, které kompilátor klidně zpřehází a přesune přes ně přiřazení?
Proletl jsem jenom zacatek a nikde nevidim, ze byste pri manipulaci s flag/FLAG promennymi pouzival zamky. Vyrobil jste priklad na nedefinovane chovani a pak vam z toho celkem pochopitelne muze lezt naprosto cokoliv. Resenim neni zahodit zamykani a ponechat volatile, nybrz presny opak: zahodit volatile a ponechat zamky.
Co se tyce prvni kompilace, ktera podle vas dava "neocekavane" vysledky (tzn. celkem druha), deje se pravdepodobne to, ze prekladac vidi definice foo()/FOO()
a spocita si, ze promenna flag
se nemuze zmenit, proto jeji opakovane nacitani v 1-3 vlaknu odoptimalizuje. Prekladac o dalsich vlaknech nic netusi, proto pristup k flag
musite obalit zamkem. Ale jak rikam, to co jste napsal je ukazkove nedefinovane chovani, takze i kdyby vam to vyplivlo kompletni dilo pana JiKa, bylo by to zcela v poradku.
Zbytek jsem neresil.
Pokud kompilátor zjevně přesouvá přístup k flag
před nanosleep()
i před puts()
, kde je jistota, že ho nepřesune před pthread_mutex_lock()
? Čím je volání té zamykací funkce tak speciální? Tohle pořád ještě nechápu.
Čím je volání té zamykací funkce tak speciální? Tohle pořád ještě nechápu.Ta funkce musí vytvořit paměťovou bariéru, aby nemohl instrukce přeskládat procesor. A o tom by zřejmě měl vědět i kompilátor, a také by neměl přehazovat instrukce přes volání této funkce. Jak je to zařízené prakticky u C/C++ nevím, každopádně synchronizační funkce nemohou být pro kompilátor funkce jako kterékoli jiné, musí s nimi zacházet speciálním způsobem.
Jak se tomu postupně snažím porozumět, mám dojem, že je to naopak. Kompilátor nemá seznam funkcí, ke kterým se musí chovat speciálně. Má seznam s opačným významem.
Na tom seznamu jsou funkce, které zaručeně nikdy nemohou mít pozorovatelný vedlejší efekt. Mezi tyto „bezpečné“ funkce patří například putchar()
, puts()
a jim podobné. Všechny ostatní funkce jsou zpočátku implicitně „nebezpečné“. U těch, jejichž kód má kompilátor bezprostředně k dispozici v době překladu, se pokusí určit, zda je lze považovat za bezpečné. Ty, jejichž kód není k dispozici nebo u nichž analýza aliasů nedává stoprocentní záruky bezpečnosti, jsou i nadále považovány za nebezpečné.
Tedy zjednodušeně řečeno, pthread_mutex_lock()
a moje_funkce_z_dynamické_knihovny()
jsou z pohledu kompilátoru obě stejně nebezpečné a k oběma se musí přistupovat v rukavičkách, zejména pokud jde o paměťové operace kolem jejich volání.
Pokud lze pthread_mutex_lock()
inlinovat, není to žádný problém. Obsahuje totiž kus asm volatile
, který zcela zjevně dělá něco s pamětí. Tedy kompilátor může inlinovat, ale rozhodně si nedovolí příslušnou paměťovou bariéru zpřeházet s ostatními instrukcemi. A už vůbec si netroufne vynechat okolní čtení z paměti.
Nejjednodušším případem je funkce static void myFunction( void ) { puts( "Stupid kidding" ); }
. Tu lze kompletně zanalyzovat, označit za bezpečnou a paměťové operace kolem jejího volání přehazovat či vynechávat jakkoliv dle libovůle.
Ještě bych rád uvedl jeden zajímavý případ: Co když standardní výstup vede na vstup jiného programu, který má s tím naším sdílenou paměť? Co když ten druhý program v reakci na zprávu zaslanou pomocí puts()
zapíše něco do sdílené paměti? Co když zapisuje právě na místo, kde leží řídící proměnná cyklu volajícího puts()
? Odpověď je jednoduchá: Výsledek něčeho takového je nedefinovaný, stejně jako jakýkoliv jiný nesynchronizovaný přístup ke sdíleným datům. O tom jsem se koneckonců (i když ne tak krkolomně) sám přesvědčil v mém pokusu níže.
Výše uvedený případ je podle mě (jen tak mimochodem) jedním z vzácných případů, kdy by se místo (náročné meziprocesové) synchronizace hodilo řídící proměnnou prostě označit (z pohledu obou procesů) jako volatile
. Tím se samozřejmě nedosáhne ani synchronizace přístupů k proměnné ani atomicity změn. Pouze se zajistí viditelnost těch změn.
To všechno ale nic nemění na faktu, že tam, kde už používám (mutex | barrier | rwlock | mutex + condvar) nepotřebuji volatile
vůbec.
Ale pořád nevím, jestli je tohle všechno pravda a jestli to už konečně chápu správně.
To všechno ale nic nemění na faktu, že tam, kde už používám (mutex | barrier | rwlock | mutex + condvar) nepotřebuji volatile vůbec.I kdyby tomu tak v současnosti bylo, nemůžeš se spolehnout, že to tak bude vždycky.
Dokud bude v pthread_mutex_lock()
několik speciálních instrukcí v asm volatile
, můžu se na to spolehnout, ať už se inlinuje nebo ne. A těch pár instrukcí tam bude vždycky.
Protože ty instrukce říkají mimo jiné „co se dělo před touto instrukcí a co se bude tím po ní nelze pomíchat“.Což nemusí vždy být relevantní. Příklad:
Je zajímavé si přečíst, jak se u kusů assembleru označují registry, které ten assembler mění. Dá se taky naprosto explicitně říct: Tento kus assembleru mění paměť, bacha na něj! To samozřejmě ještě nezaručuje, že příslušný assembler nebude vynechán. Nicméně pokud má blok assembleru v clobber listu "memory"
a zároveň je označen jako volatile
, není žádná šance, že by ho kompilátor mohl vynechat nebo zpřeházet. (Nehledě na to, že ke speciálním instrukcím v něm obsaženým by se tak či tak měl chovat slušně.)
Povídání o clobber listu je tady. A tady je něco o asm volatile
.
Je jisté, že každé synchronizační primitivum musí takový blok assembleru použít. Pak je ovšem úplně šumák, jestli je to primitivum implementováno jako makro, jako statická funkce, jako funkce z dynamické knihovny nebo jako ošklivý assemblerový hack. Žádná proměnná nemusí být volatile
, pokud se k ní přistupuje vždy pouze pod ochranou příslušného primitiva.
Jak se tomu postupně snažím porozumět, mám dojem, že je to naopak. Kompilátor nemá seznam funkcí, ke kterým se musí chovat speciálně. Má seznam s opačným významem.Jasně, zapomněl jsem, že kompilátor v době překladu nezná „instrukční obsah“ všech volaných funkcí. Ještě by mne zajímalo, jak je to s tím vícevláknovým přístupem. Zda kompilátor musí předpokládat i ovlivnění z jiného vlákna (a může tedy optimalizovat jen lokální proměnné, jejichž adresu prokazatelně nikdo nezná, a např. předpokládá, že volající dodržuje standard jazyka C, i když to nemusí být Céčkový kód), nebo zda se o vlákna nezajímá, a očekává, že pokud může být proměnná ovlivněna z jiného vlákna, musí programátor explicitně použít bariéry.
Ještě drobná poznámka: Ani náznakem jsem nemluvil o tom, že bych snad chtěl zahodit zamykání. Pouze mi není jasné, jak může to zamykání korektně fungovat bez volatile
.
Například tenhle kus kódu, který jsem už uváděl výše:
pthread_mutex_lock( &queueMutex ); ++itemsAvailable; pthread_cond_signal( &queueCond ); pthread_mutex_unlock( &queueMutex );
K čemu je mi zamykání, pokud itemsAvailable
není volatile
? Bez volatile
kompilátor klidně může přesunout inkrementaci před pthread_mutex_lock()
nebo za pthread_mutex_unlock()
, jak se mu to hodí.
Já vím, tvrdíte, že podle standardu to udělat nemůže. Nicméně v mých pokusech naprosto zjevně přesunul přiřazení přes volání funkce nanosleep()
. Tudíž jsou dvě možnosti: Buď je možné naprosto cokoliv, nebo jsou volání funkcí ovládajících mutex něčím odlišná a jakýmsi záhadným způsobem přinutí kompilátor chovat se jinak.
Přece jen ještě do třetice: Chápu-li to správně, platí tohle:
Pokud přiřazuji do globální proměnné flag
bez zamykání, nikdo mi negarantuje, kdy přesně (a v jakém pořadí) uvidím tyto změny v ostatních vláknech.
OK, jestli je tohle pravda, zní to rozumně. Nicméně například mutexy nejsou součástí jazyka ani kompilátoru. Jedná se jen o obyčejná volání funkcí. Jak mi tedy mutex (použitý bez volatile
) může správnou „viditelnost“ zaručit? Co když ji prostě zaručit nedokáže? Má snad kompilátor někde schovaný seznam funkcí, jejichž volání musí být memory fence? Podle mě žádný takový seznam nemá.
Pozor, můj vlastní příspěvek, na který reaguji, už není aktuální. Zde jsem své chybné závěry z tohoto pokusu uvedl na pravou míru. Omlouvám se za tento omyl.
Tak, celou noc jsem pročítal assembler, který vznikal z různých pokusných programů. Už začínám tušit, jak moc jsem se mýlil a jak špatně jsem pochopil výsledky mého výše uvedeného pokusu. Vše dále popsané jsem pro jistotu zkoušel pouze s -O3
.
Nic se nepřesunulo přes nanosleep()
. Zdání něčeho takového vzniklo prostě takto:
main()
přiřazuje do proměnné flag
a přiřazuje tam konstantu.flag
udělal konstantu s (poslední) hodnotou 0.Další dvě důležité poznámky:
flag
a najednou všechny funkce poctivě flag
četly. Sice pouze jednou, ale přece. (V důsledku toho pak vlákna neskončila vůbec, zůstala ve spinlocku.)flag
se pak už četla znovu v každém cyklu. Zajímavé je, že například puts()
a putchar()
čtení v cyklu nezpůsobily, zatímco pthread_yield()
, pthread_mutex_lock()
nebo something_undefined()
ano. Kompilátor tedy skutečně má nějaký interní seznam „bezpečných“ funkcí a všechny ostatní považuje implicitně za „nebezpečné“.Teď už asi konečně chápu, proč ta synchronizační primitiva fungují i bez volatile. Může se k nim přistupovat pouze dvěma způsoby:
nanosleep()
jsem si už ujasnil a jinou záměnu pořadí jsem zatím nepozoroval.asm volatile
s náležitými synchronizačními instrukcemi. Tudíž ani v tomto případě není důvod obávat se změny pořadí.Můj celkový závěr (po důkladné revizi a úspěšném pochopení výsledků mého pokusu) tedy zní: Sinuhet měl pravdu a já jsem se mýlil. Skutečně není třeba používat volatile
současně se synchronizačními primitivy.
Děkuji všem za věcnou a zajímavou diskusi a zejména Sinuhetovi za objasnění celé záležitosti. Pro mě z toho plyne poučení, že jsem si měl napřed přečíst assembler a teprve potom dělat závěry ohledně výstupu mého pokusného programu.
Klidně se může inlinovat pthread_mutex_lock()
. Kde je problém? Ten pthread_mutex_lock()
v sobě někde obsahuje kus kódu typu asm volatile
, jak už jsem psal. A tento kód zajistí nejen paměťovou bariéru, ale i slušné chování kompilátoru, který pak důležité instrukce nemůže libovolně zpřeházet mezi ostatními.
Takže čemu bych se pak měl divit? Pointa celé věci je v tom, že to synchronizační primitivum už v sobě obsahuje nějaký typ paměťové bariéry. (A ta se bude spolu s ním samozřejmě taktéž inlinovat, když už se (v nějaké hypotetické implementaci) inlinuje.) Součástí implementace té bariéry může (a nemusí) být nějaká proměnná s volatile
přístupem. Nicméně uživatele toho synchronizačního primitiva to vůbec nemusí zajímat. On sám volatile
nepotřebuje.
Zajímavé případy použití (a zneužití) volatile jsou vysvětlené ve skvělém blogpostu, na který odkazuje kolega zde v diskusi.
Dobře, třeba máte pravdu, třeba se už zase mýlím. (Nebylo by to dnes poprvé.) Ale mohl byste tedy uvést konkrétní příklad kompilátoru splňujícího normu C[++] a operačního systému splňujícího normu POSIX, jejichž kombinace povede k nefunkční synchronizaci, pokud nepoužiji volatile
? Jedině pokud taková kombinace existuje, uvěřím, že je použití volatile
(například kvůli přenositelnosti) nutné.
Zatím to ovšem stále vidím tak, že mé původní přesvědčení bylo chybné a že podmínkovou proměnnou není třeba kombinovat s volatile
.
Presne tak. pthread_mutex_lock je inline fce ktera evidentne nemodifikuje obsah volatile promenne, ani nemuze, protoze netusi kde a jak je volatile promenna ulozena. Proto kompilator optimalizovat cteni z pameti.
Takovehle "optimalizace", ktere funguji v 99.99% pripadu me uz staly desitky hodin zivota. Kdyby na miste pthread_cond_wait bylo volano makro preprocessoru, tak nemuzete o tom zdrojaku rict vubec nic.
Presne tak. pthread_mutex_lock je inline fce ktera evidentne nemodifikuje obsah volatile promenne, ani nemuze, protoze netusi kde a jak je volatile promenna ulozena. Proto kompilator optimalizovat cteni z pameti.
To je úplně jedno, co modifikuje. Ta funkce obsahuje asm volatile
se synchronizační instrukcí. Jakmile kompilátor takovou instrukci uvidí, nikdy si nedovolí ji nějak přeuspořádat. Taková instrukce se prostě vždy považuje za nebezpečnou a nikdy se nevynechá. Je přitom úplně jedno, jestli se celá ta funkce inlinuje nebo ne.
Takovehle "optimalizace", ktere funguji v 99.99% pripadu me uz staly desitky hodin zivota. Kdyby na miste pthread_cond_wait bylo volano makro preprocessoru, tak nemuzete o tom zdrojaku rict vubec nic.
To je prosím pěkně holý nesmysl. Je třeba si uvědomit, že překladač žádná makra nezná. Překladač prostě zkoumá až to, co leze z preprocesoru, a je mu zoufale fuk, jestli to vzniklo z maker nebo ne. Ta „funkce“ klidně může být makro. Pokud to bude správné a funkční makro, které dělá přesně to, co má, bude rozhodně obsahovat asm volatile
a v něm vhodnou synchronizační instrukci. Je to opět tentýž případ: Proměnnou, ke které přistupuji vždy pod mutexem, není třeba označovat jako volatile
.
Právě stahuju vaše oblíbené icc, protože gcc mi rozhodně nic takového neprovádí.
To souhlasí. Tuto optimalizaci jsem viděl pouze v ICC, nikoliv v GCC.
Pokud vláknem, které skončilo dříve myslíte t3 ve spin2.c, tak podle mě je to proto, že kompilátor znal fci FOO (a v té flag časem dojde do 0) a ne proto, že by main časem přiřadil do flag nulu.
Vlákno t3()
je můj hloupý omyl. Ten test je špatně navržený a je tam race condition. V případě gcc a -O0
čirou náhodou t3()
zvítězí nad main()
a jak tak usilovně inkrementuje flag
, nepostřehne, že tam main()
přiřadí nulu. (K přiřazení nuly dojde mezi testem a inkrementací.) Tedy uvázne.
V případě -O1
t3()
vůbec nebude flag
inkrementovat. Z kompilátoru v tomto případě vypadne taková podivná slátanina, která tam automaticky předpokládá nulu. Pokud je vaše domněnka správná, počítá kompilátor s přetečením při inkrementaci. To je zvláštní. Počítal by s tím i u 64-bitového čísla? To by tedy byla pořádná optimalizace! Ale nejspíš to tak bude...
Doteď mi to přišlo rozmně vysvětlitelné, tak mi v tom nedělejte zmatek.
Mně to doteď taky přišlo vysvětlitelné, ale bylo to pouze tím, že jsem to chápal nesprávně. Zmatek, kterému teď čelím, se mi zdá být přece jen o něco blíž pravdě.
Kompilátor zjistil, že v celém programu jen main() přiřazuje do proměnné flag a přiřazuje tam konstantu. Proto z proměnné flag udělal konstantu s (poslední) hodnotou 0.Vzhledem k tomu, že program startuje s touto proměnnou přednastavenou na 1 a neví, kdy jsou volány funkce tX, pak tuto optimalizaci lze považovat za poměrně nekorektní.
Možná. Ale assembler to říká jasně. Žádný while
cyklus tam u -O3
prostě není, žádný spinlock. Pouze se rovnou zavolá puts()
. Striktně vzato, celý ten zdroják je hodně nekorektní, takže se není čemu divit, když dává nekorektní výstup.
Chování kompilátoru je přece jen aspoň zčásti pochopitelné. Z jeho pohledu jediné, co kdy poběží, je main()
. Tedy vše, co main()
přiřadí do globálních proměnných, musí nutně být konečné. Ostatní funkce, které tam jsou, se nikde nevolají a nikde do globálních proměnných nezapisují. Kompilátor by za normálních okolností takové funkce zcela vypustil.
Jenže... Pointery na ty funkce se používají k inicializaci nějakého pole, které se pak čte. Tudíž funkce nelze jen tak vyškrtnout. Dobře tedy, kompilátor je tam nacpe. Nicméně optimalizuje je takovým způsobem, jako by před jejich voláním proběhl kompletně main()
. Vzhledem k tomu, že ty funkce jsou všechny statické a nelze je po linkování volat odjinud, jde o (téměř) pochopitelný předpoklad. Není ani správný, ani rozumný, ale je prostě pochopitelný. Kompilátor nebere v potaz, že z těch funkcí někdo uprostřed main()
udělá vlákna. To prostě není jeho starost.
Striktně vzato, celý ten zdroják je hodně nekorektní, takže se není čemu divit, když dává nekorektní výstup.Ne. Tady překladač prostě naprosto jasně optimalizoval až moc. Řekněme, že by pthread_create nebyla knihovní funkce, ale moje vlastní funkce, která tři parametry zahodí a jeden vezme jako ukazatel na funkci, tu spustí a počká až doběhne. To všechno v hlavním vlákně programu. V takovém případě mám jasné právo očekávat, že ve flag/FLAG bude jednička, protože funkci volám před tím přiřazením. Optimalizace flag = vždy 0 tedy není korektní.
Ostatní funkce, které tam jsou, se nikde nevolají a nikde do globálních proměnných nezapisují. Kompilátor by za normálních okolností takové funkce zcela vypustil.Na to se nemůžeš spolehnout. Minimálně jedna verze GCC to nedělá.
Nicméně optimalizuje je takovým způsobem, jako by před jejich voláním proběhl kompletně main(). Vzhledem k tomu, že ty funkce jsou všechny statické a nelze je po linkování volat odjinud, jde o (téměř) pochopitelný předpoklad. Není ani správný, ani rozumný, ale je prostě pochopitelný.Ne není. Jestliže dokončení main() znamená ukončení programu, pak optimalizovat, jako by před jejich voláním kompletně proběhl main(), je blbost. (A mám pocit, že tohle chování sis domyslel, přestože překladač ve skutečnosti dělá něco jiného.)
(A mám pocit, že tohle chování sis domyslel, přestože překladač ve skutečnosti dělá něco jiného.)
Píšu, co jsem viděl v assembleru. Může to být bug v kompilátoru, ale tohle si vůbec netroufám posoudit.
Neni to bug, vas program obsahuje nedefinovane chovani, takze zadny vystup nemuze byt spatne. Kompilator je komplexni hromada kodu, a kdyz mu podrazite nohy porusenim nekterych predpokladu, tak se muzete dockat necekanych vysledku.
Nic se nepřesunulo přes nanosleep(). Zdání něčeho takového vzniklo prostě takto:Především, opakuji, pořadí těch textů je výsledek toho, jak se jednotlivá vlákna poprala při jejich vypisování. O řazení/časování kódu kolem to neříká nic.
- Kompilátor zjistil, že v celém programu jen main() přiřazuje do proměnné flag a přiřazuje tam konstantu.
- Proto z proměnné flag udělal konstantu s (poslední) hodnotou 0.
- Proto právě ta vlákna, která končila podezřele brzy, žádný spinlock v sobě neměla. Optimalizace ho zrušila.
Můžete prosím uvést nějaký příklad kódu, kde synchronizační primitiva selžou kvůli nepřítomnosti volatile
?
Zámek samosřejmě neselže kvůli nepřítomnosti volatile.
Pokud tedy přistupuje k proměnné více vláken a vždy se tak děje pouze pod zámkem, tato proměnná nemusí být volatile. (Selháním zámku rozumím (například) existenci nekonzistentního stavu proměnné v okamžiku, kdy něko zámek získá.)
Samozrejme ze promenna MUSI byt volatile. Zamek nema zadny vliv na to kde a jak je promenna ulozena. Zamek akorat omezuje dobu kdy k ni muzete pristupovat. Po byste pouzil knihovnu boost a jeji atomicky decrement tak by tam ten zamek ani byt nemusel. volatile tam musi byt vzdy. To ze vam to funguje je jen shoda okolnosti. C++ kompilator nema jak zjistit ze se promenna itemsAvailable behem cyklu nemeni. Kdyby to vedel, tak by klidne tu podminku vyloucil a pro test "itemsAvailable==0" by nevygeneroval zadny kod.
Ke třem reakcím výše:
volatile
. Nějaké pseudokódy jsou sice zajímavé, ale rád bych viděl zdroják, který takovou chybu jednoznačně prokáže. Dokud ten zdroják neuvidím (a jsem si už téměř jist, že ho neuvidím), budu věřit Sinuhetovi a jeho interpretaci celé věci.volatile
podle mě není žádná náhoda a žádný dočasný stav. Když se zamykací funkce inlinují, obsahují v sobě instrukce, které si nikdy žádný překladač (ani dnes, ani v budoucnu) nedovolí zpřeházet s okolními paměťovými operacemi. No a pokud se neinlinují, je to ještě jednodušší. Pak jde prostě o neznámé knihovní funkce, které nelze „odoptimalizovat“ nebo zavolat někdy jindy.
Mimochodem, pojem neznámá knihovní funkce zas není až tak jistý, jestli mně paměť neklame existují i překladače které mají přehled co se v knihovnách děje a klidně zoptimalizují i takovéto případy.
Ano. Příkladem takových překladačů jsou GCC a ICC. To ale pořád ještě neznamená, že podmínková proměnná musí být volatile.
Ještě mám tak trochu off-topic poznámku. Není to náhodou v té Wikipedii trochu nepřesně?
Podle mě jsou v podstatě tři kombinace:
volatile int * pointer
int * volatile pointer
volatile int * volatile pointer
Z pohledu kompilátoru jsou druhá a třetí možnost v podstatě přesně totéž, protože když se může pointer nečekaně změnit, musí se tak či tak při každé jeho dereferenci znovu načíst z paměti nejen pointer samotný, ale i data, na která ukazuje. Jestli jsou referencovaná data označena jako volatile
nebo ne, to už není příliš podstatné.
Pokud by se pointer nezmenil (to lze zkontrolovat porovnanim s predchozi hodnotou), tak by se ve druhem pripade data znovu nacitat nemusela. Takze rozdil tam asi je, ale je to hodne za vlasy pritazeny priklad...
Ještě tu mám dva zdrojáky, ve kterých funkce volaná v odděleném vlákně mění lokální proměnnou jiného vlákna, na které závisí další běh programu.
Napřed bez synchronizačních primitiv:
#include <unistd.h> #include <stdio.h> #include <pthread.h> static void * setzero( void * param ) { *( (int *) param ) = 0; return NULL; } int main( void ) { int local = 1; pthread_t thread; pthread_create( &thread, NULL, setzero, &local ); while ( local ); pthread_join( thread, NULL ); puts( "The thread has finished." ); return 0; }
Tohle dopadne zruba takto:
-O0 |
-O1+ |
|
GCC | skončí | zamrzne |
ICC | skončí | skončí |
Chování GCC je zcela podle předpokladů. Proměnná není není chráněna primitivem a zároveň není volatile
, tudíž při zapnutí optimalizace původní záměr samozřejmě selže. Proč ale kód z ICC nezamrzne? Je to absurdní, ale ICC celý while
cyklus odstraní!
Naskýtá se otázka: Že by snad ICC analyzoval vytváření vláken a zjistil, co se může s tou proměnnou stát? Ne, to se neděje. Je snadné to dokázat: Když ve funkci setzero()
přiřadíme místo nuly například dvojku, while
cyklus bude odstraněn taky. A to je naprosto špatně. Je to bug v kompilátoru, nebo jen v mé hlavě? V assembleru je přesně toto:
pushl %eax #14.42 pushl $setzero #14.42 pushl $0 #14.42 pushl %edx #14.42 call pthread_create #14.2 # LOE ebx esi edi ..B1.2: # Preds ..B1.7 pushl $0 #16.24 pushl 32(%esp) #16.24 call pthread_join #16.2
Teď druhý kousek, tentokrát se synchronizačními primitivy.
#include <unistd.h> #include <stdio.h> #include <pthread.h> static pthread_mutex_t mutex; static pthread_cond_t cond; static void * setzero( void * param ) { *( (int *) param ) = 0; pthread_cond_signal( &cond ); return NULL; } int main( void ) { int local = 1; pthread_t thread; pthread_mutex_init( &mutex, NULL ); pthread_cond_init( &cond, NULL ); pthread_create( &thread, NULL, setzero, &local ); pthread_mutex_lock( &mutex ); while ( local ) pthread_cond_wait( &cond, &mutex ); pthread_mutex_unlock( &mutex ); pthread_join( thread, NULL ); puts( "The thread has finished." ); pthread_cond_destroy( &cond ); pthread_mutex_destroy( &mutex ); return 0; }
Funkce setzero()
vůbec nezamyká mutex, když signalizuje změnu podmínkové proměnné. Nicméně podle manuálové stránky je něco takového povoleno. Vše funguje zcela korektně pro všechny úrovně optimalizace u GCC i ICC. I z pohledu assembleru je vše v pořádku, lokální promměná v cyklu v main()
se skutečně vždy načte znovu.
Tady je fragment assembleru z kompilátoru Intel při -O3
, který odpovídá té smyčce v main()
:
..B1.5: # Preds ..B1.20 ..B1.19 movl 4(%esp), %eax #22.10 testl %eax, %eax #22.10 je ..B1.10 # Prob 10% #22.10 # LOE ebx esi edi ..B1.7: # Preds ..B1.5 pushl $mutex.0 #22.44 pushl $cond.0 #22.44 call pthread_cond_wait #22.18 # LOE ebx esi edi ..B1.20: # Preds ..B1.7 addl $8, %esp #22.18 jmp ..B1.5 # Prob 100% #22.18 # LOE ebx esi edi
Dokonce ani lokální proměnná tedy nemusí být volatile
, když se používá jako podmínková. V případě inlinování pthread_cond_wait()
by to dopadlo stejně, protože tato funkce musí za všech okolností obsahovat speciální instrukce a tudíž i blok asm volatile
s kouzelným slovem "memory"
v seznamu změněných registrů.
Začínám být čím dál pevněji přesvědčen, že podmínková proměnná nikdy nemusí být volatile
a že toto chování překladačů se ani v budoucnu nemůže změnit.
local
nemůže před použitím v cyklu změnit, a proto může cyklus odstranit. V tom kódu stejně nemáte zaručeno, jestli kód v novém vlákně proběhne ještě před začátkem cyklu, nebo až v jeho průběhu – a nemůžete to ani nijak ovlivnit, výsledek závisí na náhodě. ICC vám z těch různých „správných“ výsledků běhu programu vybere jeden a ostatní neumožní, ale to není špatně, protože i ten jeden výsledek je správný vzhledem k tomu, co je napsáno ve zdrojáku. Pokud tedy hodnota může být změněna z jiného vlákna, musíte ji chránit zámkem. Pak překladač pozná, že kód toho vlákna není jediný, kdo tu hodnotu může změnit, a nemůže provést příslušnou optimalizaci.
A jak to kompilator pozna? Jak pozna ktera promenna patri ke kteremu zamku? Jak pozna, ze fce pthread_mutex neco zamyka? Co kdyz zavolam QMutex->lock() ? Pokud autor prispevku chce neco optimalizovat pro "rychlost", tak je nejlepsi tam vubec zadny zamky nedavat. Flag definovat jako volatile a pouzit atomicky decrement z knihovny boost.
Pokud autor prispevku chce neco optimalizovat pro "rychlost", tak je nejlepsi tam vubec zadny zamky nedavat. Flag definovat jako volatile a pouzit atomicky decrement z knihovny boost.To není zrovna moc dobrá optimalizace pro rychlost. Optimalizace pro rychlost znamená používat synchronizaci co nejméně – zámky vám rozsekají kód na bloky kódu mezi zámky,
volatile
jej rozseká na bloky kódu mezi užitím příslušné proměnné, při častém používání té proměnné tedy až na jednotlivé instrukce. Což bude samozřejmě pomalejší.
volatile
– volatile
by ta proměnná měla být, pokud se opravdu mění mimo program, tj. je to třeba HW mapovaný do paměti. Pokud jde „jen“ o souběžný přístup vláken jednoho programu, stačí zámky – to pak umožní překladači optimalizovat úseky kódu uvnitř zámku, případně by nějaký budoucí překladač mohl optimalizovat i kód s ohledem na vlákna (zjistí, že kód je sice chráněn zámkem, ale ani souběžné vlákno nemůže hodnotu změnit, a tudíž může opět provést optimalizaci).
Tiskni
Sdílej: