Uživatelé, když povýší svou hardwarovou platformu na novější a výkonnější CPU s větším počtem rychlejších jader, očekávají, že jejich aplikace poběží rychleji. Více jader by mělo redukovat průměrnou zátěž CPU a tím zrychlit zpracování. V mnoha případech však aplikace rychleji neběží a zatížení CPU je většinou stejné jako na původním CPU. Se špičkovými CPU je dokonce možné pozorovat rušení, které brání determinismu. Proč se to děje a co s tím lze dělat? Odpověď zájemci najdou v tomto článku.
Tady je slíbená odpověď: při stavbě aplikace je třeba využívat škálovatelnost. Nemá-li aplikace architekturu, která je schopna využívat výhody multijádrového prostředí, většina aplikací v RTOS (Real-Time Operating System) poběží téměř stejně na jednojádrovém procesoru i čtyřjádrovém procesoru s meziprocesovou komunikací (v protikladu s očekáváním, že aplikace RTOS by se měla rozprostřít lineárně a na čtyřjádrovém procesoru běžet čtyřikrát rychleji než na jednojádrovém). Bez škálovatelného systému totiž tři ze čtyř jader nebudou využita. Dokonce i kdyby aplikace sama požadovala využívat vícenásobná jádra, pro dosažení optimální škálovatelnosti je nezbytné pro daný systém přesně optimalizovat celou její architekturu: přístup k paměti, I/O, strategii využívání mezipaměti (cache), synchronizaci dat atd.
1. Úvod
Ačkoliv žádný systém neposkytuje lineární škálovatelnost, je možné pracovat na dosažení teoretického limitu pro každou aplikaci. Tento článek identifikuje klíčové architektonické strategie, které zaručí nejlepší škálovatelnost pro aplikace na bázi RTOS. Prozkoumá architekturu CPU, vysvětlí, proč běh aplikace na procesoru s novějšími nebo výkonnějšími jádry nedává očekávané zvýšení výkonu, popíše, jak redukovat efekt vzájemného ovlivňování a poskytne doporučení pro modifikace hardwaru k omezení vlivu úzkých profilů.
Obr. 1. Tradiční architektura: UMA (rovnoměrný přístup k paměti)
Tento článek se týká systémů, kde současně běží běžné aplikace pracující mimo reálný čas a aplikace, které pracují v reálném čase. Aby byl dodržen determinismus v aplikacích reálného času, neměly by ideálně sdílet žádný hardware s běžnými aplikacemi. Avšak současně je užitečné mít na obou stranách k dispozici paměťové prostory a synchronizační události.
Avšak není možné dosáhnout obojího. Buď je celý počítač vyhrazen pro aplikace reálného času, ale je třeba se spoléhat na protokol sběrnice pro výměnu dat s běžnou aplikací, nebo jsou oba typy aplikací ve stejném počítači, ale ty budou muset sdílet sběrnici CPU a mezipaměť cache. V současné době jsou procesorová jádra mnohem rychlejší, než je přístup do paměti a k I/O, takže v důsledku konkurence v přístupu k těmto zdrojům vzniká rušení běhu aplikací mezi sebou.
Je zde ještě jedna důležitá věc při zvažování, zda použít vícejádrové procesory. Různá programová vlákna v jedné aplikaci většinou sdílejí stejné proměnné, takže přístup k těmto proměnným by měl být synchronizován, aby byla zajištěna konzistence dat. Není-li to upraveno v programovém kódu, procesor to provádí automaticky, ale protože nezná celou logiku aplikace, nebude to provádět optimálně a bude vytvářet mnohá čekání. Tato čekání jsou důvodem, proč aplikace nemusí nezbytně běžet rychleji na dvou jádrech než na jednom.
Tento článek nejprve ozřejmí architekturu CPU ve vztahu k přístupu k vyrovnávacím pamětím, pamětím a I/O. Potom vysvětlí, jak na sebe vzájemně působí programová vlákna a jak může návrh programu vylepšit výkon vícejádrového procesoru. Nakonec poskytne praktické ukázky zmíněné problematiky s doporučením, jak mohou být tyto úkoly řešeny.
Většina zde uvedených technických informací vychází z dokumentu What Every Programmer Should Know About Memory od Ulricha Dreppera z firmy RedHat [1]. Ačkoliv jde o článek z roku 2007, stále jej lze doporučit k přečtení.
2. Architektura CPU
2.1 Tradiční architektura: UMA (rovnoměrný přístup k paměti)
V modelu UMA (Uniform Memory Access) jsou všechna jádra připojena na stejnou sběrnici, zvanou Front Side Bus (FSB), která je spojuje na severní můstek (northbridge) čipové sady. Paměť RAM a její ovladač jsou zapojeny do stejného můstku, tedy do northbridge. Veškerý ostatní hardware je připojen na jižní můstek (southbridge), který je připojuje k CPU prostřednictvím northbridge.
Na tomto návrhu je zřejmé, že northbridge‚ southbridge a RAM jsou zdroje sdílené všemi jádry, a tudíž jsou sdílené jak aplikacemi, které pracují v reálném čase, tak aplikacemi, které pracují mimo reálný čas. Navíc mají ovladače RAM pouze jeden port, takže v jednu chvíli může mít přístup k RAM pouze jedno jádro.
Frekvence CPU byly po mnoho let stále zvyšovány bez výrazného zvyšování ceny, ale u pamětí tomu tak nebylo. Permanentní paměť (jako např. hard disk, HD) je velmi pomalá, takže bylo zavedeno užívání RAM, která procesorům dovoluje přístup k datům bez čekání na přístup k HD.
Dostupná je také velmi rychlá statická RAM (SRAM), ale ta je extrémně drahá, a tak může být použita jen v malém objemu (několika megabajtů). To, co se běžně v počítačích nazývá RAM, je dynamická RAM (DRAM), která je mnohem levnější, ale také mnohem pomalejší než SRAM. Přístup k DRAM zabere stovky cyklů CPU. V případě vícejádrových procesorů je patrné, že FSB a všechny přístupy k DRAM představují nejužší místo v tradiční architektuře.
2.2 Architektura NUMA (nerovnoměrný přístup k paměti)
Aby se odstranilo úzké hrdlo tvořené FSB a RAM, byla navržena nová architektura s vícenásobnými moduly DRAM a vícenásobnými sběrnicemi pro přístup k nim. Každému jádru je umožněno mít vlastní modul RAM. Také southbridge pro přístup k I/O může být duplikován, takže různá jádra mohou pro přístup k hardwaru použít různé sběrnice. Pro aplikace, které pracují v reálném čase, by NUMA měla mít výhodu, že nesdílejí zdroje s aplikacemi, které pracují mimo reálný čas.
Obr. 2. Architektura NUMA (nerovnoměrný přístup k paměti)
Původně byla NUMA vyvinuta k propojení mnohonásobných procesorů, ale protože v současnosti mají procesory stále více jader, musela být rozšířena a použita uvnitř procesoru.
Návrh architektury NUMA však s sebou nese nový problém: proměnné jsou umístěny v jediném paměťovém modulu RAM, ale mohou k nim potřebovat současně přistoupit různá jádra. Přistoupení k proměnným, které jsou připojeny k cizímu jádru, může být mnohem pomalejší a aplikace by měly být pro tuto architekturu vyvinuty specificky, aby byly správně použity. Používání této architektury lze doporučit pouze pro aplikace, které jsou pro ni vyvinuty, ale když počet jader v systému přesáhne čtyři, dostává se FSB v architektuře UMA snadno do přetížení a může způsobit ještě větší zpoždění. Proto bude NUMA architektura pro rozsáhlé stroje.
K tomu, aby se odstranily nevýhody architektury NUMA, jsou některé stroje postaveny s uzly několika procesorů, které sdílejí jeden modul RAM. V tomto případě používají aplikace pouze jádra uvnitř jednotlivého uzlu, aby je míjela nevýhoda NUMA s přístupem k paměti a pracovaly bez zpoždění.
Tento článek nepůjde do větších detailů této architektury, protože v současnosti není v RTX podporována.
2.3 Paměti a mezipaměti (cache)
Přístup k DRAM je pro CPU ve srovnání se SRAM pomalý (průměrně stovky cyklů pro přístup k jednomu slovu). Proto obsahují procesory (CPU) SRAM jako mezipaměť (cache), která je organizovaná v několika úrovních. Když jádro potřebuje přistoupit k datům, tato data budou zkopírována z hlavní paměti (DRAM) do jeho nejbližší mezipaměti, takže k nim může přistoupit mnohonásobně rychleji (obr. 3).
Obr. 3. Paměti a mezipaměti: nahoře uspořádání se čtyřmi jádry a dvěma úrovněmi mezipaměti, uprostřed uspořádání se čtyřmi jádry a třemi úrovněmi mezipaměti, z nich L2 je exkluzivní, a dole uspořádání s třemi úrovněmi cache, z nichž L2 je sdílená jádry v uzlu
První úroveň cache (L1) má oddělená místa pro instrukce (programový kód) a pro data (proměnné); ostatní úrovně jsou jednotné. Vyšší úrovně cache jsou větší a pomalejší než první úroveň a měly by být vyhrazeny pro jádro nebo sdíleny několika jádry. Největší cache, také zvaná LLC (Last Level Cache), je běžně sdílena všemi jádry. Průměrný čas přístupu do každé úrovně cache, měřen v cyklech CPU, je v tab. 1.
Je zřejmé, že obrovský výkon se ztrácí, kdykoliv CPU musí čekat na přístup k hlavní paměti, nejsou-li data dostupná v cache. Tato situace se nazývá cache miss (zmeškání vyrovnávací paměti). Přístup k datům v hlavní paměti je mnohem rychlejší, je-li zadáván hromadně nebo v pořadí. Ale přístup k datům a k instrukcím programu je málokdy náhodný, takže CPU bude zkoušet predikovat, které oblasti paměti budou následně použity, a zavede jejich obsah s předstihem do cache. Tato technika se nazývá předběžné načítání (prefetching) a významně zvyšuje výkon (redukuje zpoždění zhruba o 90 %).
Pro predikci, která data mají být zavedena do cache, spoléhají procesory na dva principy: dočasná lokalita a prostorová lokalita.
Dočasná lokalita znamená, že k proměnným a instrukcím je běžně přistupováno mnohonásobně v řádce. To je pravda především ve smyčkách a u proměnných, které jsou lokální k funkcím.
Prostorová lokalita znamená, že společně definované proměnné jsou obvykle použity společně a příští řádka programu pravděpodobně obsahuje následující instrukci k vykonání.
Dočasná lokalita je důvodem, proč mít cache. Kopírování dat do lokálního bufferu před použitím má smysl pouze tehdy, když se k nim bude přistupovat vícekrát. Využít výhody prostorové lokality a skutečnosti, že k datům v RAM se přistupuje rychleji po blocích, má smysl, když data nejsou vyžadována a převáděna po bytech, ale v řádkách cache, které jsou standardně dlouhé 64 bytů. Také CPU předběžně automaticky načítá následující řádku. Práce programátora, viz detailně v sekci 4, je připravit data a instrukce v pořadí, které je možné predikovat, takže předběžné načítání pracuje efektivně.
2.4 Úzká místa
Protože jsou mezipaměti (cache) drahé, jsou obvykle malé, takže ne všechna data a všechny instrukce související s aplikací se vejdou do cache. Cache je navíc sdílena všemi aplikacemi, které v jádrech procesoru běží a které jsou ke cache připojeny. Znamená to, že když aplikace nebo programové jádro zavádí příliš mnoho dat, „vyžene“ starší data, která jiné vlákno nebo aplikace může ještě chtít používat a potřebuje je znovu zavést. Více vykonávaného kódu v jádru, resp. instrukcí, bude také vypuzeno při přepínání programových vláken. Tento boj o cache se nazývá memory contention (zápas o paměť).
Last Level Cache (LLC) je sdílená jak aplikacemi, které pracují v reálném čase, tak běžnými aplikacemi, když systém je jediný socket nebo jde o systém s mnohonásobným socketem, který je konfigurován jako socket mající jak real-time, tak non-real-time jádra. Ve výsledku mohou aplikace, které nepracují v reálném čase, působit na výkonnost aplikace, jež v reálném čase pracuje, když používají rozsáhlé množství paměti, např. při přehrávání HD videozáznamu. Když je množství dat používaných aplikací nebo programovým vláknem malé, je možné vybrat CPU s cache dostatečnou na udržení veškerých dat ve vyhrazených pamětech cache, kde nebudou ovlivňovány ostatními aplikacemi.
Sběrnice Front Side Bus (FSB), která přistupuje ke hlavní paměti, je ve srovnání s CPU velmi pomalá, takže hustý datový tok z jediného jádra by mohl sám zabrat celou šířku pásma. Tento jev se nazývá bus contention (srážka sběrnice) a začíná být pozorovatelný, když má CPU čtyři a více jader. Přestože je tato sběrnice skutečně úzké místo, je běžně lepší utratit peníze za rychlejší RAM a čipovou sadu sběrnice než za rychlejší CPU. Obvykle bude v tomto případě rychlejší CPU déle čekat.
2.5 Synchronizace dat
Hlavním důvodem, proč aplikace neběží rychleji na dvou jádrech než na jednom, je synchronizace dat. Svou roli v ovlivnění doby reakce při vykonávání programu také může hrát serializace. Jádra vždy přistupují k datům prostřednictvím své výhradní cache s nejnižší úrovní. Znamená to, že když je k proměnné přistupováno ze dvou jader, musí být přítomna v cache obou jader, a když je její hodnota modifikována, musí být aktualizována v cache obou jader. CPU musí zajistit konzistenci dat pro celý systém, což může způsobit obrovská zpoždění – všeobecně platí, že při dosažení výsledku v jednom jádru je třeba zjišťovat hodnoty dat v cache dalších jader, aby byla zajištěna integrita dat. Pro údržbu této konzistence používá CPU protokol MESI, který definuje stav v řádce cache:
- modifikován: hodnota byla tímto jádrem modifikována, takže je jedinou platnou kopií v systému,
- výhradní: toto jádro je jediné, které používá tuto proměnnou, a nepotřebuje tedy signalizovat změny,
- sdíleno: tato proměnná je dostupná ve více cache a ostatní jádra budou informována, jestliže se změní,
- neplatná: žádná proměnná nebyla zavedena nebo její hodnota byla změněna jiným jádrem.
Stav každé řádky cache je udržován aktuální příslušným jádrem. Aby to bylo proveditelné, musí jádro pozorovat všechny požadavky na hlavní paměť a informovat ostatní jádra, že už má proměnné přečtené nebo modifikovalo jejich hodnotu. Kdykoliv chce jádro přistoupit k proměnné, která je modifikována v cache jiného jádra, nová hodnota musí být zaslána do hlavní paměti a pak čtena jádrem. Přístup k této hodnotě se stává tak pomalým, jako by žádná cache nebyla. Je-li do proměnné často zapisováno jedním jádrem a čtena je jiným jádrem, stane se to, co je uvedeno v tab. 2.
V tomto případě musí jádro 2 číst pokaždé hodnotu z hlavní paměti, což odstraňuje výhodu používání cache. Jádro 1 musí poslat „RequestForOwnership“ (RFO) do FSB pokaždé, když modifikuje hodnotu, a pak musí aktualizovat hlavní paměť pokaždé, když jádro 2 vyžaduje tuto hodnotu.
Přístup k této hodnotě bude mnohem pomalejší, jsou-li dvě programová vlákna na různých jádrech, v porovnání se situací, kdy obě vlákna běží na jediném jádru.
Mnohem menší problém je s instrukcemi, protože ty jsou určeny pouze pro čtení. V tomto případě není třeba vědět, kolik jader je čte. Program, který sám sebe modifikuje, sice existuje, ale takové aplikace jsou velmi nebezpečné a zřídka kdy se užívají. Proto se článek tímto případem nebude zabývat.
2.6 Vícenásobná jádra a hyperthreading
Zdá se, že mnohonásobná jádra a mnohonásobná programová vlákna na jediném jádru mají stejné využití, ale způsob, jakým využívají zdroje, je velmi odlišný. Způsob, jakým mají být použity, je totiž většinou opačný.
Obr. 4. Při hyperthreadingu dvě vlákna sdílejí stejnou úroveň 1 cache, takže v nejhorším případě by mělo každé dostupnou jen polovinu
Při vícenásobných jádrech je první úroveň cache duplikovaná a každé vlákno má svou vlastní. Znamená to, že v systému je dostupných více pamětí cache, ale potřebují být synchronizovány.
Při hyperthreadingu tato dvě vlákna sdílejí stejnou úroveň 1 cache, takže v nejhorším případě by mělo každé dostupnou jen polovinu (obr. 4).
Takže při vícenásobných jádrech programátoři musí omezit množství sdílených dat mezi programovými vlákny na každém jádru, aby zabránili synchronizačnímu zpoždění. Při hyperthreadingu je úroveň 1 cache sdílena mezi těmito hyperprogramovými vlákny. Znamená to, že když jsou data užívaná jednotlivými programovými vlákny různá, zapříčiní to střet v cache a data budou muset být zaváděna z hlavní paměti mnohem častěji. V tomto případě bude výkon zlepšen pouze tehdy, když budou nezávislé operace probíhat nad stejnou datovou sadou. To je zpravidla speciální situace, takže ve většině případů hyperthreading nezlepší výkon a lze doporučit jeho vypnutí. Také proto, že jádro i úroveň 1 cache jsou sdíleny mezi dvě hyperprogramová vlákna, mohou být obě použity aplikacemi, jež pracující v reálném čase i mimo něj.
2.7 DMA (Direct Memory Access, přímý přístup k paměti)
Přístup k I/O, který může být prostřednictvím PCI-e, USB nebo jiného typu, je řízen instrukcemi CPU. Znamená to, že když zařízení signalizuje aktualizaci dat pomocí přerušení (interrupt), CPU musí data obsloužit a poslat je do hlavní paměti. To přidá mnoho zátěže na FSB.
Obr. 5. Přímý přístup k paměti DMA
Jestliže CPU plánuje tato data bezprostředně použít, je zde nová funkce, která může být použita. Nazývá se Directed Cache Access (DCA) a data by díky ní byla kopírována jak do hlavní paměti, tak do cache CPU.
Pro vysokorychlostní záležitosti byla vyvinuta funkce DMA, Direct Memory Access (obr. 5). S použitím DMA bude zařízení signalizovat CPU, že data byla aktualizována a přímo poslána do hlavní paměti, aniž by byla vyžadována jakákoliv aktivita od CPU.
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
(dokončení příště)
(Z anglického originálu od IntervalZero přeložila firma DataPartner.)
V současné době rostoucí specializace stále existuje mnoho příležitostí pro vývoj aplikací, které potřebují pracovat v pevném reálném čase, a mnoho z nich nelze a nebo lze jen obtížně realizovat pomocí PLC. Takové příležitosti se vyskytují v oborech průmyslové automatizace, v testování a simulaci, v digitálním zpracování audiozáznamů, v energetice, ve zdravotnických systémech nebo v letectví a obraně. Zde je ideálním řešením vytvářet komplexní systémy, které potřebují využívat kvalitní rozhraní HMI (OS Windows) a současně vyžadují determinismus pro práci v pevném reálném čase. Aby to bylo možné, je třeba provést transformaci Windows na operační systém reálného času, RTOS. To lze udělat např. instalací doplňku Windows, systému RTX nebo RTX64. Tam, kde ve Windows běží časovač s maximálním rozlišením a granulitou od 1 ms, jde RTX64/RTX, jestliže to hardware umožňuje, na granulitu 1 μs. Schopnosti Windows jsou rozšířeny, aniž by se jakkoliv alternoval nebo modifikoval Windows HAL (Hardware Abstraction Layer), a je zajištěn determinismus neboli výkon aplikací v pevném reálném čase (tj. se zaručenou dobou odezvy ve zlomcích mikrosekund). Pokroková platforma pro vývoj časově kritických systémů zahrnuje vícejádrové multiprocesory x86 a x64, transformované Windows a systémy real-time Ethernetu (např. EtherCAT nebo Profinet). Ve výsledku předčí speciální real-time hardware, jako např. DSP, a radikálně redukuje náklady na vývoj systémů, které vyžadují determinismus nebo pevný reálný čas. Současně se otevírá otázka, jak optimálně programovat moderní aplikace pro vícejádrové procesory, aby vývojáři z moderního hardwaru získali pro své aplikace nejvyšší výkon. A právě tímto tématem se zabývá tento článek.
Tab. 1. Průměrná doba přístupu do každé úrovně cache, měřena v cyklech CPU
Úroveň paměti cache | Průměrná doba přístupu |
level 1 | 3 |
level 2 | 15 |
level 3 | 20 |
hlavní paměť | 300 |
Tab. 2. Do proměnné je často zapisováno jedním jádrem a čtena je jiným jádrem
Akce | Stav jádra 1 | Stav jádra 2 |
jádro 1 čte hodnotu | výhradní | neplatná |
jádro 2 čte hodnotu | sdíleno | sdíleno |
jádro 1 modifikuje hodnotu | modifikován | neplatná |
jádro 2 čte hodnotu | sdíleno | sdíleno |
jádro 1 modifikuje hodnotu | modifikován | neplatná |
jádro 2 čte hodnotu | sdíleno | sdíleno |