# Vícevláknovost v Go: jak CPU, mezipaměti a plánovač ovlivňují výkon
Současné aplikace v Go aktivně využívají paralelizmus, ale mnoho vývojářů plně nechápe, co se děje pod kapotou při práci s více jádry CPU. Tento článek rozebírá nízkourovňové mechanismy ovlivňující výkon vícevláknových programů: od specifik přepínání kontextu po koherenci mezipamětí. Vysvětlíme, proč existují atomické operace a bariéry paměti a jak se jejich vlastnosti projevují v reálných scénářích.
Mechanismy víceúlohovosti: kooperativní vs. vytlačovací
Hlavní problém jednojádrových systémů spočívá v nutnosti souběžného provádění mnoha úloh. Řešení přišlo ve dvou variantách:
- Kooperativní víceúlohovost — úlohy dobrovolně uvolňují CPU prostřednictvím systémových volání jako
sched_yield(). Zranitelnost tohoto přístupu je zřejmá: zaseknutí jedné úlohy paralyzuje celý systém (jako ve Windows 3.1).
- Vytlačovací víceúlohovost — nucené přerušení úloh podle časovače. Klíčové etapy:
- Generování hardwarového přerušení
- Uložení kontextu aktuální úlohy do RAM
- Výběr nové úlohy plánovačem
- Obnovení kontextu
Je klíčové pochopit, že kontext úlohy zahrnuje:
- Hodnoty registrů (RAX, RBX aj.)
- Ukazatel na instrukci (RIP)
- Ukazatel na zásobník (RSP)
- Registr příznaků (RFLAGS)
- Data zásobníku
Přepínání kontextu vyžaduje uložení těchto dat do operační paměti, což vytváří režii. Na moderních CPU operace context switch trvá 200–300 taktů — ekvivalent provedení desítek instrukcí.
Architektura vícejádrových systémů a hierarchie mezipamětí
S přechodem na vícejádrové procesory vznikl nový problém: jak synchronizovat přístup k sdílené paměti. Pro pochopení řešení je třeba rozebrat hierarchii mezipamětí:
L1 mezipaměť
- Velikost: 32–64 KB
- Zpoždění: ~4 takty
- Rozdělena na instrukce (L1i) a data (L1d)
- Vázána na fyzické jádro
L2 mezipaměť
- Velikost: 256 KB – 1 MB
- Zpoždění: 10–15 taktů
- Obvykle vázána na jádro
L3 mezipaměť
- Velikost: 8–40 MB
- Zpoždění: 30–50 taktů
- Sdílená pro všechna jádra
Klíčový bod: procesor sám spravuje mezipaměti prostřednictvím hardwarového kontroléru. OS a aplikace nemohou přímo ovlivňovat umístění dat. To vede k situaci, kdy různá jádra mohou pracovat se zastaralými kopiemi dat.
Protokol MESI a koherente mezipamětí
Pro řešení problému koherence se používá protokol MESI (Modified, Exclusive, Shared, Invalid). Každý řádek mezipaměti (obvykle 64 bajtů) má jeden ze čtyř stavů:
- Modified: data byla změněna v této mezipaměti a liší se od RAM
- Exclusive: data se shodují s RAM, dostupná pouze tomuto jádru
- Shared: data se shodují s RAM a mohou být v jiných mezipamětích
- Invalid: data jsou zastaralá a nelze je použít
Příklad práce při zápisu:
- Jádro A načte proměnnou → stav Shared
- Jádro B načte stejnou proměnnou → stav Shared
- Jádro A zapíše novou hodnotu → pošle invalidate požadavek
- Jádro B obdrží požadavek a převede řádek do Invalid
Tento mechanismus vytváří skryté zpoždění: při konkurenčních zápisech jádra musí vyměňovat signály přes sběrnici, což může trvat 100–300 taktů.
Praktické důsledky pro vývojáře Go
Pochopení těchto mechanismů je klíčové při práci s:
- Atomovými operacemi: instrukce
LOCK XCHGzaručují atomaritu blokováním sběrnice - Bariérami paměti:
mfenceřídí pořadí provádění instrukcí - CPU affinity: vázání gorutin na konkrétní jádra prostřednictvím
GOMAXPROCS
Příklad problému falešného sdílení (false sharing):
var data [2]int64
// Gorutina 1
for {
data[0]++
}
// Gorutina 2
for {
data[1]++
}
Navzdory práci s různými prvky pole se dostávají do jedné řady mezipaměti (64 bajtů). Při zápisu do data[0] se řada mezipaměti stane Invalid pro druhé jádro, což způsobuje neustálou synchronizaci. Řešení — přidat padding:
var data [2]struct{
value int64
_ [56]byte // Výplň do 64 bajtů
}
Co je důležité
- Přepínání kontextu vyžaduje uložení stavu do RAM, což vytváří zpoždění ~200 taktů
- Koherente mezipamětí přes MESI přidává režii při konkurenčních zápisech
- Falešné sdílení (false sharing) může snížit výkon o 2–3 řády
- Atomové operace fungují blokováním sběrnice nebo protokoly mezipamětí (v závislosti na architektuře)
- Pochopení hierarchie mezipamětí umožňuje optimalizovat struktury dat podle velikosti řad mezipaměti
Vývojáři Go se s těmito problémy často setkávají při tvorbě vysoce zatížených služeb. Například v distribuovaných systémech nesprávná práce s mezipamětěmi může vést k nečekaným zpožděním při zpracování tisíců požadavků za sekundu. Klíčový závěr: efektivní paralelizmus vyžaduje zohlednění nejen logiky aplikace, ale i specifik hardwaru.
Pro další studium doporučujeme analyzovat assemblerový výstup kompilátoru Go pomocí příznaku -S a také používat profilovače jako perf pro měření chyb mezipaměti a context switchů. Tato data pomohou odhalit úzká místa, která nejsou vidět na úrovni zdrojového kódu.
— Editorial Team
Zatím žádné komentáře.