Psal jsem to už minule, tak dneska jenom zkráceně:
1.1 klasiku ADD+ADC využívající 2x ALU je kvůli tomu nutno rozepsat na 4 instrukce využívající 4x ALU, přičemž u chytrého CPU bude trvat 3 ALU operace, u hloupého 4 ALU operace, to je v každém případě zhoršení :-)
1.2 sltu závisí na předchozím add. add z_high, z_high, tmp závisí na předchozích sltu a add. Tedy závislost se ještě zhoršila.
1.3 ALU má vstup Carry pro SUB a ADD z principu operace SUB a ADD, tím že se vstup Carry trvale připojí na 0 se nic nezjednodušilo :-)
1.4 remapping se provádí tak jako tak a pro mnoho registrů, dneska jeden navíc nehraje tak velkou roli, dříve to byl problém
1.5 To je skutečně o něco rychlejší.
FENCE nemá nic společného se souborovým flush(), v tom se často chybuje. Smysl FENCE je nastavit hranici out-of-order vykonávání instrukcí s pamětí (a io), v zásadě nějak takto:
1) Všechny instrukce pro zápis v aktuálním vlákně před FENCE musí být dokončeny už před FENCE
2) Všechny instrukce pro čtení v aktuálním vlákně po FENCE musí být zahájeny až po FENCE
FENCE.I je obdoba pro čtení instrukcí z paměti, po FENCE.I se zjednodušeně začnou instrukce v aktuálním vlákně načítat znovu.
rad se dozvim, jak nektere veci funguji na jinych procesorech. Na co jsem dost citlivy je podpora synchronizace mezi jadry a v tomhle smeru jsem se naucil pouzivat prave atomicke instrukce pro lockless algoritmy. Jak se tohle resi zde, kdyz tam podle clanku nejsou
instrukce FENCE mi prijde dost velke zjednoduseni a zpomaleni, dokazu si predstavit jak narocne musi byt zajistit zapisy pred a cteni za i kdyz to neni vzdy takhle potreba. Ve svych aplikacich mam funkce readAcquire() a writeRelease() ktere implementuji polovicni FENCE a teoreticky by mely byt rychlejsi.
Proc to pisu? Pri navrhu univerzalniho programatorskeho modelu by clovek rad vedel, co ho ceka na jinych platformach a zda nahodou nastroje ktere ted pouzivam na x64 nezpusobi ze programy budou neprenosne.
Atomické instrukce neexistují, je to zmatení pojmů, navíc atom se dneska běžně štěpí ;-) To čemu se říká atomické instrukce jsou ve skutečnosti instrukce s exclusive memory address lock, po dobu provádění instrukce má vlákno uzamčeno kus paměti.
Funkci na memory ordering si napsat NEMŮŽETE, nelze to naimplementovat softwarově, pokud si s tím chcete hrát musíte zavolat specifické instrukce procesoru. Na amd64 není pro acquire a release semantic podpora, většina operací na zamčené paměti dělá zároveň obě činnosti. Univerzální kód se dělá tak, že v aplikaci se funkce s Acquire i Release ponechají a při překladu na amd64 se makrem předefinují na ekvivalent bez a/r semantics.
Přesně tak. Bez HW podpory prostě některé operace nejde implementovat. A ta HW podpora je obecně dost složitá záležitost související se strukturou cache (koherence apod.), jaká data si mezi sebou jádra vůbec mohou efektivně posílat.
Kde přesně potřebuješ zajistit ordering? Nejaké I/O věci nebo kooperaci mezi vlákny?
Já jsem se špatně vyjádřil. Můj kód se jeden čas překládal na XBox a na IA64 a proto vznikly tyhle dvě funkce.
v gcc ted pouzivam
__atomic_load_n(var,__ATOMIC_ACQUIRE);
__atomic_store_n(var,val,__ATOMIC_RELEASE);
pripadne
__sync_synchronize();
Ve Windows pak MS extension pres klicove slovo volatile. Ale tohle je trochu magie, nikdo mi nezarucuje prenositelnost, a problemy se vetsinou poznaji az pri zatezi.
__atomic_store_n se na amd64 architektuře přeloží jako obyčejné mov, protože amd64 nedělá read ani write reordering.
XBOX360 dělá read i write reordering na úrovni hardware CPU, jde to úplně mimo programátora, takže tam by se __atomic_store_n přeložilo s nějakou memory barriérou (asi lwsync).
Špinavou práci s vytvářením memory barrier v tomhle případě dělá gcc uvnitř atomických funkcí.
Jinak hezký článek o tématu:
https://msdn.microsoft.com/en-us/library/windows/desktop/ee418650%28v=vs.85%29.aspx
Mně akorát tehdy překvapilo, že InterlockIncrement na Xbox360 automaticky nevytváří patřičné bariery. Přišlo mi, že bez barier jsou tyhle funkce useless. Od té doby si na takové věci dávam patřičný pozor a snažím se aby veškeré knihovny fungovaly všude stejně, tedy aby increment nad atomickou proměnnou zaručoval funkci na všech možných a nemožných platformách. (a samozřejmě všechny další interlocked funkce, třeba moje oblíbená compare_exchange)
To bych netvrdil, ve spoustě situací (nějaký counter) potřebuješ jenom přičíst/odečíst nějaké číslo a dál si hraješ na vlastním písečku. I reference counting garbage collector v zásadě potřebuje bariéru až před finální dealokací.
Bariéry v zásadě potřebujou jenom atomické instrukce pro implementací synchronizačních objektů.
Ne, v základní ISA (pokud bys použil takové jádro) kromě FENCE a FENCE.I (to ale není většinou tak zajímavé, pokud tedy neděláš loader DLLek například :-) tam žádné další zajištění synchronizace není. Navíc je zapotřebí dát pozor na toto:
1) FENCE skutečně zajišťuje viditelnost provedení vybraných operací pro další vlákna
2) FENCE.I jen pro jedno vlákno!
Pokud potřebuješ klasické atomické operace, tak jedině s použitím rozšíření "A" - atomic. Tam je:
LR - Load Reserved
SC - Store Conditional
AMO - read-modify-write
není tam například klasičtější CAS (Compare and Swap), protože -podle tvůrců ISA- se LR a SC lépe implementují a jsou i obecnější, navíc
Dobrá volba, tenhle koncept Load/Store exclusive je o dost flexibilnější, lock-free a hlavně rychlejší než x86 lock prefix. Kdysi jsem dělal benchmark na ARM procesoru a tyhle Load/Store exclusive byly prakticky stejně rychlé jako jiné load instrukce, zatímco na x86 se lock inc / lock cmpxchg pohyboval na desítkách tiků. Nutno podotknout, že na Core i7 s tím Intel něco udělal a jsou teď snad 5x rychlejší.
Docela by mě zajímalo, jak bez těchto instrukcí implementují synchronizaci mezi vlákny, IMHO jsou v dnešních multijádrech nebo i s preemptivním multitasking docela klíčové...
Neřekl bych, že FENCE zajišťuje viditelnost. Ani po provedení FENCE nemusí další vlákna ty zápisy vidět. Zajišťuje jen to, že další vlákna uvidí ty zápisy ve správném pořadí (nejdříve ty před FENCE a pak ty po FENCE), ale kdy budou vidět, to se neví.
Pro zmatení nepřítele SFENCE na x86 viditelnost zajišťuje (tj. funguje opravdu jako „flush“, po provedení instrukce jsou zápisy globálně viditelné).
Tyjo fakt jsme to resili, ale ty asi neberes v uvahu, ze u RISCove pipeliny je ALU vzdycky pouzita, takze je ti celkem jedno, jestli provede NOP (ADD r0,r0,r0) nebo neco jinyho. Dtto pricitani 4 k PC, deje se porad 'za bukem' a nikomu to nevadi.
U ADD vs ADC (to asi myslis v 1.3?) je rozdil, musis dopredu vedet, jestli tam Carry davat nebo tam naopak vrazit 0. Tudiz nekdy v dobe dekodovani, kdy dojizdi predchozi operace a tudiz hrozi nutnost pouziti bypassu (tedy pokud ho procal ma).
Tak speciálně pro tebe polopaticky: RISC-V si návrh procesoru tak zjednodušil, že banální operace díky tomu nelze zrealizovat dle selského rozumu na 2 instrukce, ale na 4. Například u RISC AVR Flagy mají a tato banální operace tam nepřekvapivě trvá také 2 instrukce.
U ADDxADC se už při dekódování instrukce ví, zda bude potřebovat číst Flagy nebo ne a je to naprosto stejný problém jako závislost add na předchozím sltu.
Tak taky jeste jednou: ADDC a podobne srandicky, s tim jsme si hrali kdysi v rucne psanem assembleru, ale treba GCC to moc vyuzit neumi, resp. mu to nejde moc vnutit z normalniho ceckoveho kodu.
Tady se museli jo snazit a to jeste funguje jen kvuli supportu 128bitoveho scitani (coz neni ve standardu):
http://stackoverflow.com/questions/6659414/efficient-128-bit-addition-using-carry-flag/6659465#6659465
Prakticky to tvoje carry = tmp >> 31 (nekde dole) vede normalne na instrukci sar ;)
Re FENCE: jen pro doplnění, aby nedošlo ke zmatku. Já jsem psal, že FENCE.I (ne FENCE) se svou funkcí podobá flush, a dokonce se tak může implementovat. Není to ta nejlepší implementace, ale pro zajištění konzistence programu (nezávisí na funkci cache nebo write bufferu, který je například na ARMu) by to mohlo stačit.
To došlo :-) Pokud mám správné informace, tak FENCE.I dělá tohle
1) Zahodí obsah cache na čtení instrukcí
2) Zahodí již dopředu načtené instrukce
3) Zahodí již dopředu dekódované instrukce
4) To způsobí že následující instrukce za FENCE.I se načte z paměti
Souvislost s funkcí flush() na soubory tam žádná není. Spíše se to nápadně podobá operace cache invalidate.
Jasně může být, implementace může být různá, ten bod 1) například může být nějak optimalizován.
Ovšem chování je nápadně podobné ne?:
fflush() forces a write of all user-space buffered data for the given output or update stream via the stream's underlying write function. For input streams, fflush() discards any buffered data that has been fetched from the underlying file, but has not been consumed by the application.
Taky se nepíše, jak to je implementováno, ale sémantiku (v implementaci se například asi budou chtít vyhnout pipeline stalls, ale jde o dost mezní případ, tak maximálně u JITu to může hrát roli, teoreticky).
Ad ADD+ADC:
ten příklad je v článku naschvál, protože ukazuje vlastnosti (přednosti i zápory) NEpoužívání příznakových bitů. Toto je vlastně jeden z nejhorších případů, dobře se na tom ilustruje přístup jednotlivých ISA.
PS: navíc nemám pocit, že by ADD+ADC dokázalo vylézt z céčkového kódu, ale přiznávám, že je to jen pocit, nezkoušel jsem to napsat.