"mind-fart" - tohle slovní spojení si musím zapamatovat! ;-)
Je super jak si Linus nebere servítky...
Pro snowflakes samozřejmě nic. Zkonzultoval bych to s psychologem, přinejmenším. A nějakým poradcem pro diverzitu a inkluzi. :-P
Nič netreba konzultovať, Linus vie, čo robí. Môže to podporiť diverzitu operačných systémov a inklúziu do tímov vyvýjajúcich iné kernely.
Nebude mě nijak urážet ani když se bude vyjadřovat jak dlaždič. Ale že by mi to přišlo super, to fakt ne.
Taky se mi zdá, že by se Linus občas mohl chovat víc slušně. Resp. to mohl napsat určitě lépe bez pasivní agresivity :).
Ano, idealni stav by by, kdyby jadro a system/appky byly pouze 32bit nebo 64bit, a veskere optional instrukcni sady se vyuzivali na bazi runtime detekce.
Takze je porad micek na strane glibc, ktere ma co delat v tomto smeru - namisto toho fixovani se na imaginarni urovne.
Runtime detekce ti zere pamet i vykon cpu, takze dost casto tim, ze ve finale vyberes "lepsi" cestu kodem nic neziskas. To bys leda musel v ramci startu provadet jeste nejakou rekompilaci. Coz by prozmenu zapricinilo, ze boot by netrval (nekdy) desitky minut ale klidne hodiny. A setrvale by se to zhorsovalo s tim, jak pribyvaji dalsi a dalsi varianty.
Nehlede na to, ze specielne v kernelu si dovolim predpokladat spoustu kodu, u ktereho chces aby se provadel presne tak, jak je napsany.
To co zere vykon, je nevhodne zvolena granularita. Nebudes prece vetvit v ramci vnitrni smycky. A pokud variace nema prakticky prinos (instrukce se nepouzije ve smycce), tak je uz od pocatku zbytecne takovou instrukci pouzivat.
Jenze tohle ty principielne nemuzes vedet, dokud ten kod nespustis. Ty v dobe kompilace nevis, jestli ta vetev pobezi 1x nebo milionkrat. A nevis to ani v okamziku, kdy se rozhodujes jestli pouzit variantu A nebo B. Jiste, muzes tam zavist sofistikovanejsi algoritmus rozhodovani, ale to te bude stat jeste vic.
Ale no tak, nedelejte naivu, co ocekava, ze vsechny problemy za nej vyresi automaticky pouze prekladac.
K uspesnemu produktu je potreba trocha autorske spoluprace - vemte si za priklad treba memcpy - runtime mate parametr o velikosti kopirovane oblasti, od ktere si muzete odvodit runtime narocnost a vetvit podle kategorie, zda se jedna o malou nebo velkou kopii. V pripade velke kopie pak muzete vetvit podle velikosti dostupnych vektoru na mmx/sse/avx/avx512.
Takto to ma uz jen jedinou runtime nevyhodu - ze pouziti vetsich vektoru znamena jednak prodlevu pro zapnuti dane casti cipu (nevim zda se tyka i integer, nebo jen float cest), a druhak podtaktovani jadra (casto u avx).
Prakticky vzato - ani pouziti zdanlive optimalni instrukcni sady vam nemusi zajistit vyssi vykon, a libc by klidne mohlo zustat u konzervativnich instrukcnich sad. Moderni cpu si skrze uops s tim poradi.
Takovy runtime variabilni kod existuje v mplayer/ffmpeg (libav?) kde je dct/idct implementace vybrana pri startu podle cpu flagu, a to uz dava vetsi smysl.
Nepouziti runtime optimalizace je jenom lenost tvurce.
Některé aplikace to řeší generovaným sledem instrukcí (JIT). Nebo se natáhne připravený vybraný podkód.
EDIT: Byl jsem pomalejší. K vysvětlení memcpy výše přidám, že takhle funguje i highlevel API na Applu. Tam se neřeší, např. jaký typ stromu se má použít pro data, on si ho vybírá a předělá sám podle obsahu (jak se časem mění).
5. 12. 2024, 20:05 editováno autorem komentáře
Jenže na tenhle přístup se strašně špatně dělá support, když musíte testovat všechny možné kombinace (jestli SW dobře funguje), a pak ještě když se řeší problémy. Samozřejmě na domácí použí je to skoro jedno, ale máme tady komerční distribuce, kde si zákazník ten servis platí.
Pokud se bavime o appkach.
Loader ti muze loadnout binarku i dle mikroarchitektury. Ty headery v binarkach a rozdilne pojmenovani nejsou pro srandu kralikum. Bez rekompilace. Je mozne distribuovat x variant binarek a loader si vybere dle headeru i specifickou mikroarchitekturu.
Linuxaci tohle objevili docela pozde (na rozdil treba ehm od solarisu a blahe pameti isaexec - kompletne stranny pohled:) nebo widlim PE) ale LDcko uz to nejaky ten patek umi.
Jestli chceš někoho popíchnout, musíš napsat, že Apple tohle používá od roku 1994 a použil to zatím skoro ve všech svých výměnách procesorů (68kPPC, PPCi386, i386x64, x64aarch64, osobně jsem měl v ruce binárku se 4 architekturami PPC32, PPC64, i386, x64)). Jasně, že Solaris to měl ještě o pár let dříve ale nikoho tím nedojmeš ;-)
K meritu veci ... jemu slo o to aby se dal kernel kompilovat s "-march=native-REMOTE" = pro jinej konkretni cpu nez na kterym bezi kompilace. A pak by davalo smysl pri ty kompilaci predhodit nejakej vypis cpuid.
To ale rek bych nijak nesouvisi s kernelem samotnym, jako spis s moznostma kompilatoru.
Vyrabet kategorie ktery neexistujou samo zadnej smysl nedava, protoze kazdej jeden cpu ma jinej featureset.
BTW: Kompilovat kernel pro konkretni cpu nedava skoro zadny smysl, jakykoli rozdily budou na urovni statisticky chyby, zato kdyz neco umre, nastane okamzite problem, ze to na nicem jinym nenastartuje.
Pro vysvětlení: Linus se snaží říct, že jádro má dávno mnohem podrobnější detekci schopností CPU a interně může využívat i nuancí, které by úrovněmi architektur byly ztraceny. Tj. že různé CPU podporují různé specializované strojové instrukce a rozřazením do těchto 4 úrovní by se občas stalo, že by se zbytečně nějaká fičůra nepoužila.
A teď se na to podívej z pohledu distribuce.
Podle mě chcou udělat to, že když máš CPU co podporuje všechno z v3 tak se automaticky použije jádro zkompilované pro v3. Toto je jasný bonus, protože některé instrukce jsou šikovné a nejedná se pouze o SIMD, na druhou stranu nemá cenu dělat runtime dispatch kvůli kódu kde se použije trochu BMI2 instrukcí. Pokud máš v1 až v4 tak máš jen 4 úrovně, pro které budeš optimalizovat, a tyto úrovně reflektujou celkový pokrok v mikroarchitektuře.
v1 je v podstatě baseline (SSE2), v2 je SSE4.2 (CPU co nemá AVX2), v3 je AVX2/FMA a v4 je AVX-512. Toto je podle mě celkem dobrá granularita, kde trochu ostrouhají jen ti, co mají CPU s AVX ale bez AVX2, ale to je právě ta daň za ty úrovně.
Jádro si stejně detekuje všechny CPUID rozšíření a podle toho udělá runtime dispatch kde je to vhodné (třeba když máš AESNI a tak se použije toto pro AES, atd...). Prostě ty úrovně jsou vhodné v případě, že chci optimalizované celé jádro a ne jen části, které mají runtime dispatch.
5. 12. 2024, 23:52 editováno autorem komentáře
Jenze optimalizace na uarch se nedeji pouzitim specifickych instrukci, ale treba jinym poradim, ktere reflektuje velikost ROB a pipeline. A to ma kazda generace jinak. Pokud se nezvladne optimalizovat na tohle, tak nedava smysl pouzit instrukci ktera usetri nekde par bajtu v binarce.
Já jsem napsal v x86 asm desetitisíce a možná i statisíce řádků a řeknu ti, že přehodit nějaké 2 instrukce dnes nemá vůbec žádný efekt. Dnešní CPU jsou tak OoO, že to je úplně jedno.
Naopak co se vyplatí optimalizovat je aby CPU viděl adresy dopředu, ze kterých bude číst. Takže pokud udělám třeba XOR nějakého registru a vypadne mi z toho adresa, ze které hned další instrukce čte - tady je stall (perf: frontend stall), a nepomůže NIC kromě přepsání toho kódu tak aby tam ten XOR nebyl (toto může být třeba problém pokud mám red/black tree a ten bit pro barvu nacpu do jednoho pointeru, na druhou stranu pokud chci výkon tak nebudu používat red/black tree).
Takže, vědět jak velký je ROB a podle toho dělat nějaké optimalizace je úplný nonsense. Vědět ale třeba latence instrukcí, tady se dají dělat kouzla (třeba AVX2 kód kde použiju VPCMPEQB může mít menší latenci než AVX-512 kód, který použije tu stejnou instrukci jen kvůli tomu, že ten AVX2 kód použije SIMD registr jako masku a ne K registr).
Takže za mě optimalizovat pro konkrétní mikroarchitekturu je skoro vždycky o tom, jaké mají instrukce latenci a kolik L1/L2/L3 cache je k dispozici. Další věci jako třeba penalizace za branch misprediction jsou sice užitečné, ale neviděl jsem nikdy kód, který by z toho dokázal něco měřitelného vytěžit.
Ale tak můžeš překvapit a linknout něco, kde se opravdu optimalizuje podle velikosti ROB, atd... Takový kód bych rád viděl (pokud dělá něco užitečného) i benchmarky k tomu, a naučil se něco nového.
6. 12. 2024, 00:41 editováno autorem komentáře
Tak se treba podivej na GCC ze co dela -mtune (meni charakter generace - prave podle mikroarchitektury), zatimco -march voli subset instrukcnich sad, ktere muzou byt obsazeny ve vystupu (podle typu/modelu/submodelu procesoru).
Nebavime se o ASM, tam to neni optimalizovatelny (resp. kdo pise asm, nechce aby se do toho jakkoliv sahalo).
GCC dělá jen to, že má tabulky latencí některých instrukcí, takže je dokáže optimálně seřadit. Další z věcí je třeba znalost optimální velikosti load/store. Si pamatuju jak starší procesory měli penalizaci za 256-bit load/store, takže compiler emitoval kód, který to rozdělil na 128-bit operace, jenže toto je pořád kategorie "latence instrukcí".
Podle me si to je vedome i poctu tech internich rename registers, takze to muze pouzivat jakesi virtualni registry, byt generovany kod bude mit porad dokola pouzivat ty same nazvy zakladnich registru.
Za me je prave pocet internich (neviditelnych) registru stezejni optimalizace (u mtune) pro zvolenou mikroarchitekturu, aby byl konkretni procesor (a jeho tranzistory) vyuzity naplno.
Takze (nakonec) souhlasim, ze prerovnani kvuli latencim uz neni uplne cilovka.
Podle mě Linus nemá pravdu.
Co se týče rozšíření tak opravdu x86 nemá žádný standard úrovní rozšíření od výrobců CPU (proč taky, ty nezajímá historie), na druhou stranu dnes nikdo nepíše aplikace tak, že by detekoval každou CPUID feature a program by měl 50 různých implementací nějaké funkce, která by byla pro každou možnou kombinaci instrukcí, která ta funkce dokáže využít.
Příklad: Zrovna ta instrukce CMPXCHG16B byla přidaná hodně brzo (dřív než třeba BMI nebo BMI2) a pokud CPU má BMI2, tak má BMI i CMPXCHG16B.
Není potřeba nic hádat, stačí se podívat na mikroarchitektury nějakých 10-15 let zpět a od toho je možné odvodit úrovně rozšíření, a toto přesně GLIBC dělá. Mám pocit ale, že právě ty v1 až v4 se specializujou hlavně na SIMD, ale třeba CPU s AVX2 má AVX, F16C, CMPXCHG16B, ADX, BMI, BMI2, atd... (a toto lze odvodit právě z těch dostupných mikroarchitektur).
Takže za mě ty úrovně mají právě sjednotit různé mikroarchitektury pod nějakou úroveň aby právě těch kombinací nebylo moc. No a pokud někdo má mikroarchitekturu, která byla zrovně někde mezi v2 a v3 (ale nemá všechno co v3 požeduje) tak prostě bude mít v2.
ARM toto má definované trochu líp (ARM8.X kde X přesně definuje požadované a volitelné rozšíření CPU).
Tohle má smysl řešit z hlediska buildů, pro kterou verzi architektury je zkompilovaná - a souhlas, že výsledkem by mělo být racionální množství knihoven / programů. V jisté době byly třeba populární balíčky I386 a I686.
IMHO Linus spíš mluvil o source code - kde má smysl #ifdef AVX2 ... místo #ifdef x86_64_v2 . Takže pustím-li kompilátor s -mcpu=native, tak mi ten kompilátor přesně předá, co je na tom procesoru k dispozici, nebo s -mcpu=skylake dostanu AVX2 a dvacet dalších, ale rozhoduju se u specifické funkce podle AVX2 (bo potřebuje YMM registry), ne podle vX. Z hlediska build distribuce vyprodukuje několik kopií kódu, které jsou relevantní pro danou vX, tím, že předá daný flag kompilátoru. A pak se buď nainstaluje specifická vX nebo linker v runtime vybere specifickou vX, je-li jich několik.
Tohle by mělo platit pro obecný kód, kde výhody detekce v runtime by byly zanedbatelné nebo kontraproduktivní (cena za detekci převýší cenu za užitek). Některé knihovny (video, obrázky, machine learning, math) mají optimalizovaný kód pro více architektur a vybírá se až v runtime, buď na základě if-else a přiřazení do pointeru nebo chytřeji dynamicky v době symbol resolution).
Jenže toto přesně dostaneš a v tom je to kouzlo.
Pokud třeba použiješ gcc "--with-arch_64=x86-64-v3" tak to __AVX2__ právě bude definované, protože ten v3 level AVX2 vyžaduje. V kódu není potřeba nic upravovat.
6. 12. 2024, 00:09 editováno autorem komentáře
Ale prave v tom by se melo rozlisovat mezi statickou a dynamickou variantou.
A staticky kompilovat neco pro _vX je takove meh, at si to jako tedy delaji distribuce, ktere to lip nedokazou, a v tom je to, proc se Linus certi - protoze jadro to prave dokaze lepe resit v runtime ze co je dostupno (a z urcitych duvodu se pouziti extra registru resi vice explicitneji, viz nize).
Souvisi to s tim, ze jak se pracuje s temi rozsirenymi registry v jadru - musi se to explicitne obsluhovat, takze autor o tom musi vedet / vi o tom, protoze pak nastava jina prace u kontext switchu. Typicky je to duvod proc je pouziti FPU v jadre velke no-no, ale s prichodem uzitecnejsich veci se tam vytvoril urcity famework prave na obsluhu sse/avx/aes-ni - pro zlepseni vykonu sw-raidu nebo sifrovacich algoritmu - to jsou veci kde to autor kodu explicitne vetvi podle dostupnych cpuflags a implementace existuji na ruzne varianty.
TLDR: pokud v jadru chces pouzit sse/avx, tak se musi "odswapovat" obsah tech registru (tedy spis ulozit na stack), protoze by-default se na ne nesaha a nemeni se, a po vykonani optimalizovaneho kernel kodu je zas obnovit. Tohle samotny kompilator nezvladne resit - v jadru to chodi proste jinak.
pouzivas slovo "mikroarchitektura" spatne. Ta opravdu nerika to, ze jake instrukce CPU podporuje, ale resi hlavne JAK se instrukce vykonavaji (z pohledu prejmenovani registru, branch prediction, spekulativniho vykonavatni, atd).
Divej, výbobci x86 CPU k mikroarchitektuře přiřazujou i ty rozšíření a další kýbl věcí, takže nevím proč bych se tu měl teď hádat zrovna o tom, co všechno si lze pod tím slověm "mikroarchitektura" představit. Když řeknu Zen 4 tak každž ví, že ten CPU má AVX-512 a je to právě ta mikroarchitektura, která to podporuje.
Nechápu proč bych tu měl řešit něco takového sorry :)
"Když řeknu Zen 4 tak každž ví, že ten CPU má AVX-512"
No vidis a presne tohle neni pravda. Az se rozhodnou vyrabet nejaky ten atom, tak z toho treba zrovna to avx skrtnou, ale arch bude stejna.
Pokud AMD udělá vlastní "ATOM" tak mikroarchitektura právě stejná nebude...
AVX-512 stejně musí povolit OS, můžeš to odstranit pomocí mikrokódu (tak jak to udělal Intel), ale takto můžeš zmenšit i ten ROB popř. penalizovat nějaké jiné instrukce (třeba dopad nějakých microcode updatů na latenci gather instrukcí u Intelu),.
Kdybych to shrnul, tak jistý si nemůžeš být dnes asi ničím, ale každá mikroarchitektura něco přináší a třeba rozdíl Zen4 a Zen5 ve vykonávání AVX-512 je propastný, a tady jde o uarch (mění se latence, mění se max. IPC, atd...)