Aktuální vydání

celé číslo

07

2019

Řízení dopravy a budov

celé číslo

Jak optimálně nastavit výkon vícejádrových operačních systémů (2. část – dokončení)

3. Paměť a programování násobných jader

3.1 Metody využití vyrovnávací paměti cache

Používání cache je kompletně řízeno procesorem a nemůže být modifikováno programátorem. Ale způsob, jakým je řízení implementováno, může mít rozsáhlý dopad na provádění programu: data dostupná v různých pamětích cache mohou být rozdílná nebo data v cache nižší úrovně mohou být duplikována do cache vyšší úrovně. Jsou-li data duplikována, zmenšuje to dostupnou kapacitu cache vyšší úrovně, ale když dvě jádra přistupují ke stejné proměnné sdílené touto cache (vyšší úrovně), mohou ji použít k synchronizaci proměnných namísto celého přístupu do hlavní paměti.

Lze shrnout, že cache je vždy plná, což bude pravda, jakmile počítač poběží několik minut. Takže libovolná data přidaná do cache způsobí v jiné řádce cache, že budou jiná data „vypuzena“, tzn. vykopírována zpět do následující úrovně cache, která tato data otočí a pošle je zpět do hlavní paměti, jestliže ta již tato „vypuzená“ data neobsahuje. Obvykle je to tak, že nejstarší použitá hodnota v cache bude „vypuzena“, avšak některé novější procesory mají komplexnější kalkulace pro výběr, kterou hodnotu „vypudit“. Procesory mají instrukce pro management paměti, které mohou být použity k posílení nebo vyhnutí se těmto vlastnostem cache, ale použití těchto funkcí je hardwarově a aplikačně velmi specifické a vyžaduje vývojářský čas. Tyto funkce zde nebudou popisovány, ale jsou vysvětleny v úvodu dokumentu od Ulricha Dreppera [1]. 

3.2 Exkluzivní a sdílené cache

V odstavci 2.3 již bylo popsáno, že k dispozici jsou mnohonásobné architektury cache, které mají značně příznivý dopad na výkonnost. Výběr, na kterém jádru by které programové vlákno mělo běžet, by měl záviset na tom, jak programová vlákna na sebe vzájemně působí a které cache jsou použitelné.

Ideálně by programová vlákna, která sdílejí mnoho proměnných, měla běžet na stejném jádru a naopak vlákna běžící na různých jádrech by neměla sdílet proměnné. Ovšem kdyby to bylo možné, malé aplikace by měly běžet na jediném jádru. Větší aplikace vyžadují mnohonásobná jádra pro běh různých modulů a ještě potřebují sdílet data a synchronizovat moduly.

Programátoři proto potřebují identifikovat, které proměnné jsou nebo nejsou sdílené, a snaží se držet mnoho lokálních proměnných ve výhradní cache a současně mají sdílené cache pro držení sdílených proměnných. Ve specifickém případě RTX, kde jsou dva operační systémy, každý se svými jádry, by ideální situace byla mít tři úrovně cache: úroveň 1 výhradní pro každé jádro, úroveň 2 oddělenou v cache jádra RTX a cache jádra Windows a poslední úroveň cache sdílenou všemi jádry (obr. 6). Tímto způsobem nemohou aplikace ve Windows „znečistit“ cache RTX úrovně 2 a programová vlákna RTX na různých jádrech mohou sdílet proměnné, aniž by se spoléhala na hardware, který je sdílen s prostorem aplikací, jež neběží v reálném čase. Tato ideální situace je však platná pouze tehdy, nepřesáhnou-li nejvíce běžně užívané proměnné v aplikacích RTX velikost cache na 2. úrovni. 

3.3 Optimalizace při deklaraci proměnných

K tomu, aby byly využity výhody optimalizace cache a vícenásobných jader, je třeba identifikovat různé typy proměnných a různým způsobem s nimi zacházet. Jsou to tyto různé typy proměnných.

Proměnné používané jediným jádrem. Ty by se měly objevovat pouze v cache úrovně 1.
Kdyby se „nastěhovaly“ do L2/LLC, nemohou se objevovat v cache L1 vícenásobných jader.

Read-only, tj. proměnné určené pouze pro čtení. Jsou inicializovány pouze na začátku a poté se nikdy nemění. Tyto proměnné mohou být sdíleny mnohonásobnými jádry, bez potíží s výkonností.

Proměnné většinou read-only.

Často modifikované proměnné. Proměnné, které jsou často modifikovány mnohonásobnými jádry procesoru nebo jedním jádrem ve sdíleném stavu, budou mít pomalý přístup, proto by měly být seskupovány, aby se zabránilo ovlivňování ostatních proměnných.

Jsou k dispozici definice pro kompilátor, které mohou zajistit potřebné zařazení a vyjádřit typ proměnných, ale zlepšení může být dosaženo zejména deklarováním vlastností proměnných.

K proměnným je přistupováno po řádkách cache v délce 64 bytů, takže nejlepší je ujistit se, že v řádce cache je dostupný pouze jeden druh proměnné. Aby toho programátor dosáhl, mohou být proměnné seskupovány ve strukturách, jejichž délka je násobkem 128 bytů.

Celková velikost struktur by měla být co nejmenší a proměnné budou zarovnány do struktur (lze předpokládat 64bitové zarovnání na 64bitových systémech), takže typy menších proměnných by měly být seskupeny společně, např. dva typy int nebo čtyři typy short.

Předběžné načítání bude zavádět následující řádku cache, takže první proměnná, jež má být použita, by měla být na začátku struktury a proměnné by měly být deklarovány v pořadí, v jakém jsou použity.

Proměnné typu „většinou read-only“ a „často modifikované“, které jsou užívány společně, by měly být seskupeny dohromady, protože všechny mohou být aktualizovány v jediné operaci.

Nejsou-li pravidla dodržována, může nastat neočekávaná situace, nazývaná false sharing. To se přihodí, když proměnná, která by měla být read-only nebo použita jediným jádrem, je nastavena jako invalid, protože je na stejné řádce cache jako proměnná, která je modifikovaná jiným jádrem. V tomto případě přístup k první proměnné bude pomalý, přestože by pomalý být neměl. Bude-li programátor držet rozdílné typy proměnných na různých řádcích cache, zajistí tak, že taková situace nenastane.

Přístup ke sdíleným a často modifikovaným proměnným může být pomalý, a dokonce předmětem vzájemného ovlivňování, je-li FSB přetížená. K těmto proměnným by nemělo být přistupováno z vláken, která jsou deterministická a vyžadují malé časové odchylky ve vykonávání programu. Aby bylo dosaženo malé časové odchylky, může být užitečné vytvořit k jádru specifické přenosové proměnné, ke kterým je přistupováno z nejvíce deterministického vlákna, a nechť vlákno nižší priority synchronizuje tyto proměnné se sdílenými proměnnými. 

3.4 Optimalizace přístupu k proměnným

Je-li zadána práce s velkými datovými sadami, které přesahují velikost cache, může být užitečné psát program způsobem, který optimalizuje použití cache. Tyto operace mohou být použity při analýze obrazů nebo při jiných operacích nad velkými maticemi. Zmíněná sekce přichází v úvahu pouze tehdy, je-li matice příliš velká na to, aby se vešla do cache. Protože bude nutné data zavádět z hlavní paměti, měla by být používána takovým způsobem, který využívá výhody cache a pamětí RAM:

  • je-li to možné, měla by datová sada být rozbita do malých sad, které se vejdou do cache, a všechny operace na jediné takové sadě budou provedeny před přesunem k další datové sadě,
  • k datům je třeba přistupovat v pořadí, které je definováno v paměti, protože předběžné načítání zkracuje dobu zavádění.

Pro příklad možné modifikace programu je uvedeno násobení dvou čtvercových matic A a B, každá s 2 000 řádky a sloupci.

Matice A je v paměti definována jako pole polí. Takže proměnné jsou organizovány způsobem uvedeným v obr. 7.

Standardní program k vykonání násobení bude velmi jednoduchý, používá tři smyčky:

 for (int i = 0; i < 2000; i++) {

   for (int j = 0; j < 2000; j++) {

     for (int k = 0; k < 2000; k++)

        Result [i] [j]+ = A[i] [k]*B[k] [j]

   }

}

 

S tímto programem jsou matice zpracovávány různými způsoby, viz obr. 8.

S touto jednoduchou logikou jsou data v první matici zpracována pouze jednou a v pořadí, v jakém se nacházejí v paměti. Ale ve druhé matici jsou data do procesoru zaváděna mnohokrát a v pořadí, které vypadá náhodně.

V takových případech by programátor měl zkusit rozdrobit výpočty do menších datových sad, které se vejdou do cache L1d, a zkusit ukončit užívání této datové sady před přesunem k další.

 int SetSize = DataSetSize / CellDataSize; // 64 / 8

int Iteration = 2000 / SetSize;

for (int i = 0; i < Iteration; i+ = SetSize) {

   for (int j = 0; j < Iteration; j+ = SetSize) {

     for (int k = 0; k < Iteration; k+ = SetSize) {

        for (int i2 = 0, i2 < SetSize; i2++) {

           for (int j2 = 0, j2 < SetSize; j2++) {

             for (int k2 = 0, k2 < SetSize; k2++)

                Result[i][j + SetSize*i2 + j2] +=

                   A[i][k + SetSize*i2 + k2]*B[k][j+ SetSize*k2 + j2];

           }

        }

     }

   }

}

 

Abychom algoritmus zjednodušili, mohou být matice představovány pointery:

for (int i = 0; i < Iteration; i+ = SetSize) {

   for (int j = 0; j < Iteration; j+ = SetSize) {

     for (int k = 0; k < Iteration; k+ = SetSize) {

        for (int i2 = 0, i2 < SetSize; i2++) {

           R2 = Result[i][j + SetSize*i2];

           A2 = A[i][k + SetSize*i2];

           for (int k2 = 0, k2 < SetSize; k2++) {

             B2 = B[k][j + SetSize*k2];

             for (int j2 = 0, j2 < SetSize; j2++)

                R2[j2]+ = A2[k2] + B2[j2];

           }

        }

     }

   }

}

 Takové modifikace programu mohou zredukovat dobu výpočtu o 75 %, což může být velký rozdíl. Ale protože dělají program složitější a zavádějí nové proměnné, jsou užitečné pouze tehdy, jsou-li zpracovávaná data dost rozsáhlá a tato vylepšení vyváží prodloužení doby potřebné k programování.

 3.5 Optimalizace předvídatelnosti (predikovatelnosti) programu

V případě optimalizace předvídatelnosti programu jde o mnohem méně práce se zdrojovým kódem. Kompilátor zná správné seřazení a pravidla optimalizace a bude je automaticky používat. Avšak jakákoliv chyba v predikci instrukcí zapříčiní mnohem delší zpoždění, než je z důvodu přístupu k datům, protože instrukce potřebují být dekódovány před tím, než jsou použity CPU. Proto, je-li to možné, by měl programátor omezit chyby v predikci.

Rozvětvení programu se lze vyhnout na výchozí cestě jeho výkonu. Jsou-li v programu splněny očekávané podmínky, nemělo by to vést k rozvětvení nebo odskoku. Má-li podmínka pravděpodobnou hodnotu, měl by následovat pravděpodobný kód výkonu programu a ten bude předběžně načten. Je to např. tehdy, když se kontrolují chyby nebo nesprávné parametry. Lze předpokládat, že věci se správně vyvíjejí a poskytovaná data jsou většinu času platná.

V tomto případě by vyhodnocenou podmínku měly následovat běžné operace a programový kód pro obsluhu chyby může být zaveden daleko později.

Existuje ještě jiná optimalizace, která je vhodná pro stav CPU zvaný Out Of Order execution (OOO), nesprávný výkon programového kódu. To nastane, když CPU detekuje, že jsou dvě instrukce nesouvisející a vykonání druhé může být začato jako první, aby se ušetřila doba zpracování. To běžně nemá účinek na výkon, s výjimkou, kdy druhá instrukce bude používat zdroje (jako např. čtení z hlavní paměti), které první bude také potřebovat, a proto význačně zpožďuje vykonání první instrukce. Instrukce CPU pro paměťový management mohou předejít OOO, kdyby tento problém nastal. Ale ještě jednou, použití těchto instrukcí vyžaduje precizní pochopení hardwaru a toho, jak paměťový management funguje.

 3.6 Serializovaný programový kód

Plno částí aplikací, včetně těch, které používají knihovny, je voláno několika programovými vlákny běžícími na různých jádrech. Nelze se úplně vyhnout serializovanému kódu, aniž by byla duplikována většina aplikace a kód operačního systému, což výkon poškozuje ještě více.

Operační systém používá interní mutex, zvaný spinlock, k tomu, aby zabránil současnému přístupu k serializovaným částem kódu. Spinlock umožní programovému vláknu počkat na potřebné zdroje bez uvolnění jádra pro další vlákno. Všeobecně se používá pouze pro velmi krátkou dobu čekání, mnohem kratší, než je perioda plánovače úloh.

Nejnovější špičkové procesory nově umějí omezit dopad serializovaného kódu pomocí transakčních registrů. Operace uskutečněné v transakčních registrech nejsou bezprostředně použity v běžných registrech. Místo čekání na zdroj, který používá spinlock, druhé programové vlákno provádí výpočty v transakčních registrech, ty pak uplatní, až se zdroj uvolní, a to pouze tehdy, jestliže v mezičase nebyl obsah čtecích registrů změněn. Ve více než 90 % případů nebývají čtecí registry modifikovány a druhé vlákno běží bez čekání.

 4. Řešení problémů v praxi

4.1 Pokles výkonu vícejádrového procesoru

Problém: Aplikace, která pracuje v reálném čase, byla příliš pomalá nebo využívala CPU téměř na 100 %, proto bylo přidáno samostatné jádro s RTX, aby navýšilo výkon. Avšak výkon se nezlepšil, nebo dokonce klesl.

Příčina: Pravděpodobně jde o zpoždění v synchronizaci dat. Jestliže aplikace nebyla vyvinuta pro vícenásobná jádra a mnoho proměnných je sdíleno programovými vlákny z různých jader, budou nadbytečně používat cache a výkon pravděpodobně klesne vzhledem k neustálému přistupování k hlavní paměti nebo k mechanismu práce s vyrovnávacími pamětmi.

Možná řešení: Pravděpodobně nejlepším řešením je modifikovat aplikaci podle návodu v kapitole 3 tohoto článku. Řešení pomocí přidání jader v tomto případě nefunguje, nevede ke zlepšení výkonu bez modifikace softwaru. Alternativním řešením může být zvýšení frekvence procesoru s jediným jádrem a pokud možno i frekvence celé RAM.

 4.2 Oddělení proměnných v programovém vláknu nezlepšilo výkon

Problém: Aplikace běžící v reálném čase byla modifikována tak, aby mohla běžet na vícejádrovém procesoru. Programová vlákna v každém jádru nyní sdílejí pouze omezený počet proměnných, a přesto se zdá, že tyto modifikace výkon významně nezvýšily.

Příčina: Jestliže separace proměnných používající různá jádra nezvýšila výkon, pravděpodobně dochází k nesprávnému sdílení. Již v sekci 3.3 bylo vysvětleno, že programátor se musí ujistit, že proměnné, které nejsou sdílené, nejsou na stejné řádce cache jako proměnné, které sdílené jsou. Proměnné nejsou zaváděny do cache individuálně, ale jsou zaváděny do řádky cache o délce 128 bytů.

Možná řešení: Je třeba zrevidovat deklaraci proměnných tak, jak bylo vysvětleno v sekci 3.3. Je zapotřebí seskupit různé druhy proměnných v různých strukturách tak, aby zabíraly celé řádky cache. Tím si bude programátor jist, že jsou proměnné v paměti oddělené a nebudou se vzájemně ovlivňovat.

 4.3 Procesy na různých jádrech s RTX64 se ovlivňují

Problém: Dvě nezávislé aplikace byly nastaveny tak, aby běžely na dvou oddělených programových jádrech s RTX, ale když jedno z nich provádí složité výpočty, způsobuje to zpoždění doby reakce na druhém jádru.

Příčina: Dvě oddělená jádra neustále sdílejí zdroje, přestože se udělalo vše pro to, aby se jádra separovala. Přestože I/O jsou použity na oddělených sběrnicích PCI-e a není zde žádná sdílená paměť, poslední úroveň cache (LLC) CPU je stále používaná oběma aplikacemi a stejně tak i FSB pro přístup k I/O a RAM. Zde může být střet jak LLC, tak FSB, nebo obojího.

Když aplikace provádí složité výpočty, pravděpodobně také používá velké množství dat. Tato data budou zanášet LLC a nutit další aplikaci, aby požadovala data znovu z RAM. Zavádění těchto dat rovněž vytvoří velký provoz na FSB, což může způsobit přetížení. Jestliže další aplikace též potřebuje přístup k datům v RAM, tyto přístupy budou trvat déle.

Možná řešení: Prvním řešením by bylo omezit rychlost provádění výpočtů první aplikací, aby se zabránilo zanesení cache a přetížení FSB. Dalším řešením je zvětšit cache CPU a pro modul RAM zvýšit frekvenci, aby se omezil dopad složitých výpočtů.

Na několika velmi špičkových procesorech už Intel vyvinul funkci zvanou Cache Allocation Technology (CAT), která vyhrazuje prostor v cache pro určitý procesor. Intel také oznámil vznik metody nazývané Memory Bus Allocation (MBA), která vyhrazuje šířku pásma FSB pro určené jádro. Uvedené funkce jsou v současné době dostupné pouze v poslední řadě procesorů nejvyšší třídy. Obě jsou nyní podporovány v RTX64 3.4 (viz text na rastru).

 4.4 Několik jader s Windows způsobuje delší dobu odezvy

Problém: Aplikace RTX byla implementována na nové CPU, které je rychlejší a má více jader. Nová jádra byla přiřazena pro Windows. Nebyla provedena žádná změna na straně systému Windows ani na straně RTX, a přece jsou nyní zpoždění v aplikacích RTX.

Příčina: Je to způsobeno střety na sběrnici. FSB, která spojuje jádra procesoru s RAM, je omezený zdroj, mnohem pomalejší než jádra procesoru. FSB je sdíleno všemi jádry a i procesor s jedním jádrem ho může zcela vytížit. Pravděpodobně je to aplikace ve Windows, která spotřebovává většinu dat. Tato aplikace přistupuje k datům prostřednictvím FSB tak rychle, jak to je možné s použitím jader dostupných z Windows. S větším počtem jader s Windows se poměr šířky pásma datových přenosů v FSB pro RTX zmenšuje a narůstají zpoždění v přístupu k hlavní paměti.

Možná řešení: Ideálním řešením by bylo přesunout se na platformu NUMA nebo rezervovat šířku pásma pro RTX. Ale změnit platformu na NUMA by vyžadovalo závažné modifikace v aplikacích a NUMA není dosud v RTX podporována. Intel už oznámil dostupnost metody pro alokaci potřebné šířky pásma FSB, která se nazývá Memory Band­width Allocation (MBA), a ta už v RTX64 3.4 podporována je.

Řešení dosažitelná v současné době vedou k omezení počtu systémových jader. Aby se zabránilo těmto střetům (výskyt střetů na sběrnici začíná být velmi důležitý, když CPU obsahuje více než čtyři jádra), je třeba zvětšit přenosové pásmo v FSB. Naše doporučení je, jestliže je to možné, pořídit rychlejší RAM a ujistit se, že systém podporuje odpovídající vyšší frekvenci.

 5. Závěr

Operační systém reálného času umožňuje vývojářům psát aplikace, které pracují v reál­ném čase, stejným způsobem, jako se píší aplikace pro Windows, a ovládat plánování a separaci zdrojů s Windows. Ale některé ze zdrojů, které jsou stále sdílené, jako procesor, cache a šířka pásma FSB, se v nových procesorech stávají úzkým místem. To způsobuje potíže s výkonem, jež se často zdají nevysvětlitelné nebo nepochopitelné a celkově mají dopad na škálovatelnost.

V tomto dokumentu bylo prošetřeno, co způsobuje takové potíže s výkonem, jak může volba hardwaru vylepšit výkon a která řešení s těmito potížemi pomohou. Také byly uvedeny nové metody CAT a MBA od Intelu, které dále vylepšují výkon a jsou nyní podporované v RTX64 3.4. S těmito informacemi a technikami je možné optimalizovat aplikace na vícejádrových systémech a zvýšit škálovatelnost pro lepší komplexní výsledky napříč organizací.

 Literatura:

[1] DREPPER, Ulrich. What Every Programmer Should Know About Memory [online]. 21. 11. 2007, 114 s. [cit. 2019-01-11]. Dostupné z: https://akkadia.org/drepper/cpumemory.pdf 

(Z anglického originálu od IntervalZero přeložila firma DataPartner.)

Obr. 6. Dva operační systémy, každý se svými jádry má tři úrovně cache: úroveň 1 výhradní pro každé jádro, úroveň 2

oddělenou v cache jádra RTX a cache jádra Windows a poslední úroveň cache sdílenou všemi jádry

Obr. 7. Uspořádání matice A

Obr. 8. Postup násobení matic A, B

 

Podpora CAT a MBA v RTX64 3.4

Intel již přidal do svých novějších špičkových procesorů některé nové vlastnosti, jako jsou např. metody pro alokaci cache (CAT), alokaci paměťové sběrnice (MBA) a transakční registry pro redukci ovlivňování výkonnosti a ochranu kritických programových vláken proti rušení. RTX64 3.4 nyní podporuje nové vlastnosti jejich připojením k nastavování prio­rity a afinity real-time programových vláken, aby vývojáři mohli použít novější procesory bez potřeby dělat změny ve svých programech.

 Komponenty s největším dopadem na systém

Procesor

Co systém reálného času vyžaduje nejvíce, je stabilita, ne síla výkonu nebo úspora energie. Procesory Atom poskytují dobrý výkon, avšak mobilní procesory nebudou nikdy velmi stabilní. Rysy jako hyperthreading, stavy boost a sleep mohou být zakázány, jestliže je to umožněno.

Nezávislé aplikace RTX by měly užívat oddělená jádra a mít oddělené jádro od Windows. Ovšem aby se zabránilo potížím se střety, doporučuje se omezit počet jader na maximálně čtyři nebo zajistit, aby žádná z aplikací na Windows neměla velký objem zpracovávaných dat.

Velikost a rozdělení cache často může mít větší dopad než frekvence CPU. Všeobecně, zvětšení cache přinese rychlejší vykonávání programu, ačkoliv jak se zvětšuje velikost cache, tak se prodlužuje i doba přístupu. Je-li velikost cache stále menší než velikost datové sady a je hodně používán přístup k RAM, větší cache bude zkracovat celková zpoždění. Jestliže velikost cache je vždy větší než datové sady, je její zvětšení kontraproduktivní.

Jestliže aplikace RTX užívá vícenásobná jádra a má sdílenou cache na úrovni 2 pouze jádry s RTX, může být užitečné držet se poznatků popsaných v sekci 2.3.

 Čipová sada a RAM

U náročných aplikací může být úzkým místem přístup k paměti, přitom velikost RAM není problém. Je třeba se ujistit, že veškerá data, která aplikace používají, se do ní vejdou; žádný další paměťový prostor na tom nic nemění. Frekvence paměti a frekvence sběrnice čipové sady (chipset) jsou kritické a budou určovat, jak rychle může CPU přistoupit ke všem užívaným datům. Proto je třeba zvážit, jak velkou frekvenci může vývojář navrhnout.

 Zařízení I/O

Existuje ještě další možné úzké místo, kterého by se vývojáři měli vyvarovat, přestože existují dostatečné kapacity ze strany přístupu k RAM.

Jakékoliv zařízení, které je připojeno k RTX, by mělo mít své vlastní napojení PCI-e, aby se zabránilo zpoždění. Nové procesory podporují pouze PCI-e, takže zařízení PCI jsou ve skutečnosti seskupována a spojována do linek PCI-e hardwarovými čipy. Je-li to možné, mělo by to být zakázáno, protože další čipy budou způsobovat nekontrolovatelná zpoždění. Pouze zařízení přímo zapojená do čipové sady budou poskytovat dobrý výkon.

Veškerá nastavení možností sleep a power saving by měla být pro odpovídající linky PCI-e zakázána. To se nastavuje v BIOS. BIOS sice je zpravidla uživatelsky modifikovatelný, avšak správně konfigurovaný BIOS by měl být požadován od dodavatele počítače.