0xDEADBEEF

RSS odkazy

mikro-neefektivity

22. 7. 2024 #low level #optimalizace #VCMI

Vrtám se teď v C++ zdrojácích VCMI – reimplementace klasické tahovky Heroes of Might and Magic 3 – ve snaze to trochu zrychlit. Když jsem hrál na svém starém laptopu, na gigantické mapě tahy 7 počítačových oponentů trvaly 2 minuty. To bylo dost času na to, abych nastaroval perf, jeden tah sbíral data a potom, co jsem odehrál svůj tah, jsem další dvě minuty zkoumal, kde jsou slabá místa.

Je jich nepřeberně.

Neefektivní datové struktury, O(n) kód, kde by měl být O(1), třídy a structy jsou příliš velké, používající neefektivní reprezentaci dat. A pak tisíc dalších maličkých mikro-neefktivit.

Jako příklad si vezměte funkci, co kontroluje, jestli se daná souřadnice nachází v mapě.

bool CMap::isInTheMap(const int3 & pos) const {
  return pos.x >= 0 && pos.y >= 0
      && pos.z >= 0 && pos.x < width
      && pos.y < height && pos.z <= (twoLevel ? 1 : 0);
}

Je to maličká věc, která se přeloží na 19 instrukcí. Co na ní může být špatného? Jen v ní se stráví 1.3% času a tak je to kandidát na optimalizaci.

Můžu ji přepsat do následující podoby:

bool CMap::isInTheMap(const int3 & pos) const {
  return (uint32_t)pos.x < width
      && (uint32_t)pos.y < height
      && (uint32_t)pos.z <= (twoLevel ? 1 : 0);
}

Tuhle techniku jsem poprvé viděl ve zdrojácích PHP. Pokud je pozice negativní, při konverzi na int bez znaménka se z ní stane velká pozitivní hodnota, vždy větší než všechny znaménkové inty. Ušetříme si tak 3 porovnání s nulou. Kompilátor tuhle transformaci nemůže udělat sám, protože neví, že velikost mapy je vždy pozitivní.

Výsledná binárka je pak tohle:

mov    0x44(%rdi),%edx
xor    %eax,%eax
cmp    %edx,(%rsi)
jae    exit
mov    0x40(%rdi),%ecx
cmp    %ecx,0x4(%rsi)
jae   exit
movzbl 0x48(%rdi),%eax
cmp    0x8(%rsi),%eax
setae  %al
.exit
ret

Jen 11 instrukcí a to ušetří asi 0.5% času.

Dál: int3 argument se předává jako reference, tedy jako pointer, takže když volající má x, y, z souřadnice v registrech, musí je zkopírovat na zásobník, jen aby je funkce isInTheMap o pár instrukcí dál z paměti zase vytáhla. Nepřestavuje to velké zpomalení, L1 cache má latenci obvykle 3-5 taktů, ale čtení z registru je zadarmo.

Protože jde o struct, podle System V ABI by se tři inty poslaly ve dvou registrech a funkce by musela udělat nějaké shifty, ale to je teď vedlejší.

Dál: protože jádro VCMI se kompiluje jako knihovna, která se dynamicky linkuje jak ze serveru, tak i z klienta, volání probíhá přes plt.

call CMap::isInTheMap@plt

V plt stubu je pak něco jako

jmp  *0x79845a(%rip)

Další úroveň indirekce, další zpomalení. Podle perfu se jen v plt pahýlech strávilo 0.2% času.

Ale hlavní je, že i takto jednoduchá funkce nebude inlinována. A konvence volání říká, že některé registry nemusí přežít a tak je volající buď nemůže použít a alokátor registrů má svázané ruce, nebo si je musí uložit na zásobník. Zase je to další malá komplikace a další malá neefektivita.

Když se podívám na binárku, je vidět, že jen 6 instrukcí dělá užitečnou práci. Tolik by jich v ideálním případě zůstalo po inlinování.

mov    0x44(%rdi),%edx   // argument
xor    %eax,%eax         // návratová hodnota
cmp    %edx,(%rsi)       // užitečná práce
jae    exit              // užitečná práce
mov    0x40(%rdi),%ecx   // argument
cmp    %ecx,0x4(%rsi)    // užitečná práce
jae   exit               // užitečná práce
movzbl 0x48(%rdi),%eax   // argument
cmp    0x8(%rsi),%eax    // užitečná práce
setae  %al               // užitečná práce, jmp po inlinování
 .exit
ret                      // návratová hodnota

Navíc kompilátor může provádět kontextovou optimalizaci. Pokud je po inlinování vidět, že se souřadnice z nemění (např. dívám se na sousední políčka v jedné úrovni, kompilátor z zkontroluje jen jednou a pak dvě instrukce zcela vynechá.

To je jeden případ mikro-neefektivity na kterou jsem narazil. Teď si představte, že jich je tam tisíc, ale přesto jsou zcela zastíněné čtením z paměti, protože data jsou zkrátka prostě příliš velká a nevejdou se rozumně do cache. V jedné funkci, hned po volání isInTheMap, následuje mov co poprvé sáhne na dané struct TerrainTile a jen na tomhle jednom movu se stráví 0.56% celkového času.

komentáře
22. 7. 2024mikro-neefektivity
12. 6. 2024Prefix popcount
29. 1. 2024PHP by byl lepší jazyk, kdyby byl napsaný v Rustu
4. 10. 2023DIY hash index v SQLite
18. 5. 2023PHP refcount jako optimalizace [EN]
21. 4. 2023Podmíněná inkrementace v x86
23. 1. 2023realloc, mremap & virtuální paměť
11. 12. 2022PHP FFI pro kompaktní data a parsování
5. 12. 2022Degenerované fulltext hledání v SQLite
8. 8. 2022Fingerprintování virtuálního procesoru
9. 6. 2022Protip: Eliminujte zbytečné maskování
11. 4. 2022Běží obě strany unixové pipe paralelně?
30. 1. 2022Propustnost dvoukanálových pamětí
10. 12. 2021Míchání intů a ulongů
7. 11. 2021Iterace směrem k nule
5. 11. 2021ulong -> float
20. 9. 2021Malé řazení ve vektoru - pcmpistrm
4. 9. 2021Paralelní souborové I/O přes io_uring
18. 8. 2021Osmibajtový hash
15. 8. 2021Dodatek k bitonic sortu a optimalizaci
19. 7. 2021Inserting 100 million rows in sqlite on old garbage hardware so fast it's not even funny
6. 7. 2021Bitonic sort
2. 6. 2021Scala 3 a klíčové slovo inline
29. 4. 2021Optimalizace SplPriorityQueue
2. 4. 2021Struct of arrays & array of structs [EN]
12. 3. 2021Jak přesunout 1 až 16 bajtů do SIMD registru
10. 3. 2021Užitečné bash nástroje
16. 2. 2021Rychlosti a latence SSD disků
3. 2. 2021Proč je M1 tak rychlý?
16. 1. 2021Floating point triky
3. 1. 2021Asociativní pole v jazyce D
20. 12. 2020GC a granularita alokací v jazyce D
28. 11. 2020Top-k v PHP 7 a 8
12. 11. 2020Top-k
2. 11. 2020Průnik bitmap množin
25. 10. 2020Proč nemám rád Go [EN]
15. 10. 2020Samooptimalizující se kolekce
9. 10. 2020Java, Scala, D, Javascript, PHP a regulární výrazy #8
26. 9. 2020Kešování na webu
22. 9. 2020Bez virtuální paměti
7. 9. 2020Rekurze, regexy a kombinátory parserů
29. 8. 2020Kolik registrů má x86?
10. 8. 2020Rychlý průnik množin a Jaccardův index přes SIMD instrukce
25. 7. 2020Parsování JSONu nemusí být (úplně) pomalé ani v Go
14. 7. 2020Java, Scala a regulární výrazy #7 - zero width positive lookbehind
2. 7. 2020Jazyk D, stringy, paměť a garbage collection
14. 6. 2020RSS v PHP
25. 4. 2020Parsování JSONu je neuvěřitelně pomalé, ale nemusí být
10. 3. 2020Jak dostat HTML z HTML DOMu v PHP
14. 1. 2020Scala: dočasné mutable a immutable mapy
20. 12. 2019Porovnání Javy a Scaly v porovnávání
25. 8. 2019Jak rychlá je reflexe na JVM?
21. 5. 2019Scala, mapy a čítače
13. 5. 2019Další novinky ve Scale 2.13
14. 4. 2019Parciální funkce ve Scale a dvojité vyhodnocování
10. 4. 2019Minimalistické zvýrazňování syntaxe
13. 3. 2019Jak rychlý je čas (v Javě)?
21. 2. 2019Really simple RSS [EN]
26. 1. 2019Java, Scala a regulární výrazy #6 - znovupoužití Matcher objektu
12. 1. 2019Poznámky ke slajdům k přednášce, kterou jsem nikdy nepřednášel
4. 1. 2019Minifikace HTML5
24. 12. 2018Java IO & NIO - jak nejrychleji načíst soubor z disku
3. 11. 2018Průnik množin ve Scale
23. 10. 2018Čím nahradit Scala.XML
11. 10. 2018Novinky kolekcí ve Scale 2.13
4. 8. 2018Co vlastně benchmarkujete?
28. 7. 2018Java, Scala a regulární výrazy #5 - posesivní regexy a StackOverflowError
26. 7. 2018EDGE, TRIPS a hon za vyšším ILP
24. 6. 2018Striktní a líné jazyky
21. 6. 2018Syntéza datových struktur a programů
9. 6. 2018ISPC, SPMD a SIMD
6. 4. 2018Konec Intelu, konec Moorova zákona, konec světa takového jaký známe
31. 1. 2018Meltdown, Spectre a branch prediction
28. 12. 2017Poznámky k výkonu
19. 12. 2017Java, Scala a regulární výrazy #4 - líné regexy a StackOverflowError
5. 12. 2017Java, Scala a regulární výrazy #3 - rychlost
27. 11. 2017Java, Scala a regulární výrazy #2 - rychlost
26. 11. 2017Java, Scala a regulární výrazy
21. 11. 2017Java - zostřování obrázků
7. 10. 2017Koherence cache a atomické operace
5. 10. 2017Nejrychlejší hashmapa pod sluncem
23. 9. 2017Dekódování x86 instrukcí
21. 9. 2017Energetická efektivita programovacích jazyků
17. 9. 2017B-stromy, persistence, copy-on-write a Btrfs
15. 9. 2017Rust a parsery
13. 9. 2017Velké stránky paměti
11. 9. 2017Deanonymizace agregovaných dat
9. 9. 2017Optimalizující kompilátor
7. 9. 2017Identifikace zašifrovaných videí
5. 9. 2017Heterogenní persistentní kolekce
3. 9. 2017JVM Anatomy Park
1. 9. 2017Datacentrum z mobilů
30. 8. 2017Propustnost pamětí
28. 8. 2017Branch prediction (implementace v procesorech)
Starší články publikované na funkcionálně.cz.
16. 11. 2017Dobrý odhad vydá za tisíc přesných počtů
15. 6. 2017Hořící křemík & násobení matic
1. 6. 2017Jazyk D a radix sort
29. 5. 2017Monoid
27. 5. 2017Von Neumannovy lži
25. 5. 2017Radix merge sort
23. 5. 2017Iterace křížem krážem
23. 5. 2017Kompaktní stringy
17. 4. 2017Mikrobenchmarky jsou těžké
19. 2. 2017Lokalita v grafech a negrafech
7. 2. 2017Vstříc řazení v lineárním čase
27. 1. 2017Maximálně negativní
24. 1. 201799.00000000000000009 problémů s floating point čísly
15. 1. 2017Závislost je špatná (pro váš program i pro váš hardware)
1. 11. 2016Od pohledu dobrý, aneb jak najít skoro stejné obrázky mezi dvěma miliony souborů za méně než deset minut
14. 10. 2016Sketches - slajdy
30. 9. 2016YOLO tree
25. 9. 2016diff a stromy
18. 9. 2016diff a komprese
9. 9. 2016Mýtus o O(1) paměti
22. 8. 2016Mergeselect
14. 8. 2016Persistentní datové struktury
26. 7. 2016Úvod do podivností moderního hardwaru, které vás budou budit ze spaní
1. 6. 2016Escape analysis
29. 5. 2016Jak rychle řadit a šetřit čas
21. 5. 2016Čím více se věci mění, tím více zůstávají stejné
29. 4. 2016How to sort out your life in O(n) time
11. 4. 2016Jak řadit v lineárním čase, křísit mrtvé a dosáhnout osvícení
26. 3. 2016Dualismus hardwaru a softwaru, strojů a virtuálních strojů
4. 3. 2016Někdy je nejchytřejší nedělat nic chytrého (další kapitola nekonečného příběhu o optimalizaci)
1. 2. 2016Kolize hashů pro mírně pokročilé
6. 1. 2016Střeva databází
12. 12. 2015I ve Scale se dá psát rychlý generický kód za použití typeclass
8. 12. 2015Jaccardovo tajemství - jak počítat podobnost množin pomalu, jak ji počítat rychle a jak při výpočtu podvádět
10. 11. 2015limit/offset stránkování nemusí být pomalé
30. 10. 2015PHP compaction hell: Kdo neamortizuje, spláče nad O(n²)
7. 10. 2015Inkluzivní cache, mnoho vláken a problémy
18. 9. 2015Jak JVM volá virtuální metody, jaká temná božstva musí vzývat, aby to bylo aspoň trochu rychlé
16. 9. 2015Grim hardware realities of functional programming
20. 7. 2015Generování kódu za běhu (ve Scale)
6. 7. 2015Pár poznámek k pár poznámkám o sloupcových databázích
25. 5. 2015L1I cache a iTLB - když ani spekulace nepomůžou
17. 5. 2015Za jak dlouho procesor vynásobí tisíc čísel
29. 4. 2015Bez typů se obejdeme, ale...
28. 3. 2015Jak optimalizovat deoptimalizací
21. 1. 2015Hyper-threading aneb "Jak sakra může běžet víc vláken na jednom jádře?"
2. 1. 2015XPath - co, proč a hlavně jak?
24. 11. 2014Branch prediction moderních procesorů
30. 10. 2014Procesory a jejich architektura (sebrané spisy)
19. 8. 2014Types will carry you over the Monads
15. 5. 2014PHP DOM, SimpleXML a Matcher
23. 4. 2014Výsledky PHP kvízu
28. 3. 2014PHP kvíz (aktualizováno)
9. 3. 2014Haldy nejsou tak velké, jak se se zdají být
7. 11. 2013Poznámka k Moorovu zákonu a rychlosti procesorů
25. 9. 2013Tak jak je to s tou rychlostí procesorů a pamětí?
6. 9. 2013Co je vůbec objektové a funckionální programování?
15. 8. 2013Všechno, co jste kdy chtěli vědět o promise, yield a generátorech v PHP, ale báli jste se zeptat
13. 8. 2013Jak vlastně psát asynchronní kód
21. 7. 2013Monády aneb Jak jsem se naučil nedělat si starosti a mít rád Haskell
11. 7. 2013JVM: Epizoda V – Paměť vrací úder
30. 6. 2013Paralelismus a asynchronnost
20. 6. 2013Kolik paměti zabírají PHP pole a objekty?
4. 6. 2013JVM a pohled objektům pod sukně
24. 5. 2013Scala - Novinky ve verzi 2.10
6. 5. 2013Rekurzivní sizeOf pro JVM
26. 4. 2013Async SQL
22. 4. 2013Anorm
13. 4. 2013Cost per element/entry in various well-known Java/Guava data structures
26. 3. 2013What every programmer should know about memory
22. 3. 2013Jak z funkcí implementovat objektový systém
20. 2. 2013Velikost objektů na JVM - Scala a specialiazce polí
30. 1. 2013Velikost objektů na JVM - Scala @specialized
25. 1. 2013SLICK
25. 1. 2013Velikost objektů v Javě - mapy
21. 1. 2013Velikost objektů v Javě
24. 12. 2012Fluent interface a funkcionální programování
16. 12. 2012Play! framework
3. 12. 2012Out of the Tar Pit
1. 1. 1970Paperlog
píše k47 (@kaja47, k47)