Zpět na domů

Optimalizace assembleru: jak jedno písmeno zrychluje kód 3násobně

Experimentální výzkum demonstruje, jak nahrazení 16bitových operací 32bitovými v assembleru eliminuje falešné závislosti mezi registry, zrychluje provedení dělení trojnásobně. Analýza práce Register Alias Table a vlastností kanalizace.

Tajemství 3násobného zrychlení: mikrooptimalizace v assembleru
Advertisement 728x90

# Jak změna jedné písmene v assembleru ztrojnásobí rychlost dělení: technický rozbor

Jedna změněná značka v instrukci assembleru zkrátí čas provádění operace dělení o 66 %. Experiment s reálnými měřeními na moderních CPU ukazuje, jak falešné závislosti mezi registry proměňují potenciál out-of-order provádění v úzké místo. Rozkládáme architektonické nuance x86-64 ovlivňující výkon.

Architektonické vlastnosti registrů x86-64

Moderní procesory x86-64 využívají hierarchii registrů s dědičnou strukturou. Registr rax (64 bitů) zahrnuje:

┌───────────────────────────────────────────────────────────────┐
│                           rax (64 bitů)                      │
├───────────────────────────────┬───────────────────────────────┤
│          (horních 32)         │           eax (32 bitů)       │
│                               ├───────────────┬───────────────┤
│                               │               │   ax (16 bitů)│
│                               │               ├───────┬───────┤
│                               │               │ah (8) │al (8) │
└───────────────────────────────┴───────────────┴───────┴───────┘

Klíčový moment: zápis do nižších částí registru (ax, al) neovlivňuje vyšší bity, zatímco operace s 32bitovými registry (eax) úplně přepíšou horních 32 bitů. Tato vlastnost vytváří skryté závislosti, které jsou kritické pro pipeline.

Google AdInline article slot

Instrukce dělení div/idiv využívají pár registrů:

┌──────────┬────────────────┬──────────────┬─────────────────────┐
│ Velikost │ Dělenec        │ Podíl v      │ Zbytek v            │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 16-bit   │ dx:ax          │ ax           │ dx                  │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 32-bit   │ edx:eax        │ eax          │ edx                 │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 64-bit   │ rdx:rax        │ rax          │ rdx                 │
└──────────┴────────────────┴──────────────┴─────────────────────┘

Před provedením dělení je nutné vynulovat vyšší část páru. Standardní vzor:

mov edx, 0
mov eax, 536700
div dword [denominator]

Experimentální data: rozdíl 3,7×

Testovací smyčka zpracovávala 2 073 600 iterací (1920×1080), simulující vykreslování pixelů. Výsledky v taktech na iteraci (po odečtení režijních nákladů smyčky):

Google AdInline article slot
┌────────┬──────────────┬──────────────┐
│        │ idiv (čist.) │ div (čist.)  │
├────────┼──────────────┼──────────────┤
│ 16-bit │ 22.2         │ 22.2         │
├────────┼──────────────┼──────────────┤
│ 32-bit │  6.0         │  6.0         │
├────────┼──────────────┼──────────────┤
│ 64-bit │ 26.2         │ 24.1         │
└────────┴──────────────┴──────────────┘

Překvapení: 32bitové operace probíhají 3,7krát rychleji než 16bitové při stejných vstupních datech. Důvod nespočívá v složitosti algoritmu dělení, ale ve vlastnostech provádění na úrovni mikroarchitektury.

Mechanismus falešných závislostí

Procesory využívají Register Alias Table (RAT) pro mapování logických registrů na fyzické. Při tom:

  • Zápis do 32bitového registru (mov edx, ...) úplně přepíše hodnotu a přeruší závislosti na předchozích operacích
  • Zápis do 16bitového registru (mov dx, ...) vyžaduje spojení nových dat s vyššími bity, což vytváří falešnou závislost

V 16bitové variantě řetězec závislostí vypadá takto:

Google AdInline article slot
div word → merge → mov dx → merge → div word → ...

Každá iterace musí čekat na dokončení předchozí kvůli nutnosti spojování registrů. V 32bitové variantě se tento řetězec přeruší, což umožňuje procesoru pipeline provádění.

Klíčové metriky výkonu

  • Latency: 21–22 takTů pro 16bitové div (čas do připravenosti výsledku)
  • Throughput: 6 takTů pro 16bitové div (interval mezi spuštěním operací)

Když jsou operace závislé, limitujícím faktorem je latency. U nezávislých operací je to throughput. Poměr 22/6 ≈ 3,7 vysvětluje pozorované zrychlení.

Ověření hypotézy: změna jedné písmene

Experiment potvrdil teorii. Nahrazení 16bitových instrukcí 32bitovými v 16bitovém kontextu:

; Bylo
mov dx, 0x0008
mov ax, 0x2B7C

; Stalo se
mov edx, 8
mov eax, 0x2B7C

Vedlo k radikální změně výsledků:

┌───────────────────────┬──────────────┬──────────────┐
│                       │ idiv (čist.) │ div (čist.)  │
├───────────────────────┼──────────────┼──────────────┤
│ 16-bit (mov dx)       │ 22.2         │ 22.2         │
├───────────────────────┼──────────────┼──────────────┤
│ 16-bit (mov edx)      │  7.0         │  7.0         │
└───────────────────────┴──────────────┴──────────────┘

Jedno písmeno e v instrukci odstranilo falešnou závislost a zkrátilo čas provádění z 22,2 na 7,0 takTů – přesně na úroveň throughput děliče.

Co je důležité

  • Výběr velikosti registru je kritický: Použití 32bitových operací pro přípravu 16bitového dělení odstraní zbytečné závislosti
  • Latency vs Throughput: Při přítomnosti závislostí výkon určuje latency, ne throughput
  • Architektonické nuance: Porozumění RAT a partial register merging je nutné pro nízkourovňovou optimalizaci
  • Děliče vrací zbytek rychleji: V x86 je implementováno paralelní výpočet podílu a zbytku, což umožňuje použít edx dříve než rax
  • Testování je nezbytné: Teoretické odhady často nesouhlasí s reálnými měřeními kvůli mikroarchitektonickým vlastnostem

Praktické doporučení pro vývojáře

  • Při práci s dělením vždy používejte 32bitové operace pro vynulování vyšších částí registrů, i když pracujete se 16bitovými daty
  • Vyhněte se míchání operací s různými velikostmi registrů ve smyčkách
  • Pro výkonově kritické části kódu kontrolujte assemblerový výstup kompilátoru
  • Berte v úvahu, že throughput děliče je vyšší než jeho latency, ale pouze při absenci závislostí
  • Na moderních CPU (Ice Lake a novější) kontrolujte specifikace uops.info pro aktuální metriky

Experiment ukazuje, jak hluboké porozumění mikroarchitektuře umožňuje nalézt neobvyklé body optimalizace. Změna jedné písmene v instrukci, která na první pohled vypadá nesmyslně, přináší trojnásobné zrychlení díky odstranění umělých omezení pipeline. Pro vývojáře systémového softwaru je to připomínka: i v tak nízkourovňových konstrukcích jako assemblerové instrukce se skrývají možnosti optimalizace, dostupné pouze při porozumění „železu“ pod kapotou.

— Editorial Team

Advertisement 728x90

Číst dál