Psaní High-Performance spravovaných aplikací: Úvod
Gregor Noriskin
Tým výkonu Microsoft CLR
Dne
Platí pro:
Microsoft® .NET Framework
Shrnutí: Seznamte se s modulem Common Language Runtime rozhraní .NET Framework z hlediska výkonu. Zjistěte, jak identifikovat osvědčené postupy pro výkon spravovaného kódu a jak měřit výkon spravované aplikace. (19 vytištěných stránek)
Stáhněte si profiler CLR. (330 kB)
Obsah
Žonglování jako metafora pro vývoj softwaru
Modul ClR (Common Language Runtime) rozhraní .NET
Spravovaná data a uvolňování paměti
Alokační profily
Rozhraní API pro profilaci a profiler CLR
Hostování serveru GC
Dokončení
Model Dispose
Poznámka k slabým odkazům
Spravovaný kód a JIT CLR
Typy hodnot
Zpracování výjimek
Vytváření vláken a synchronizace
Reflexe
Pozdní vazba
Zabezpečení
Volání zprostředkovatele komunikace a platformy MODELU COM
Čítače výkonu
Další nástroje
Závěr
Zdroje informací
Žonglování jako metafora pro vývoj softwaru
Žonglování je skvělá metafora pro popis procesu vývoje softwaru. Žonglování obvykle vyžaduje alespoň tři položky, ale neexistuje žádný horní limit počtu položek, které se můžete pokusit žonglovat. Když se začnete učit, jak žonglovat, zjistíte, že watch každý míč zvlášť, když je chytáte a házíte. Jak postupujete, začnete se soustředit na tok míčů, na rozdíl od jednotlivých míčů. Až zvládnete žonglování, můžete se znovu soustředit na jeden míč, vyrovnáváte ho na nose, zatímco budete pokračovat v žonglování s ostatními. Intuitivně víte, kde budou koule, a můžete dát ruku na správné místo, abyste je chytili a házili. Jak to tedy vypadá s vývojem softwaru?
Různé role v procesu vývoje softwaru žonglují s různými "trojicemi"; Projektoví a programoví manažeři žonglují s funkcemi, zdroji a časem a vývojáři softwaru žonglují s správností, výkonem a zabezpečením. Vždy se můžete pokusit žonglovat s více položkami, ale jak může každý student žonglování dosvědčovat, přidání jediné koule činí exponenciálně obtížnější udržet koule ve vzduchu. Technicky vzato, pokud žonglujete s méně než třemi koulemi, vůbec nekusíte. Pokud jako vývojář softwaru neuvažujete o správnosti, výkonu a zabezpečení kódu, který píšete, může se stát, že svou práci neděláte. Když začnete uvažovat o správnosti, výkonu a zabezpečení, zjistíte, že se budete muset soustředit na jeden aspekt. Jakmile se stanou součástí každodenní praxe, zjistíte, že se nemusíte soustředit na konkrétní aspekt, budou prostě součástí způsobu, jakým pracujete. Jakmile je zvládnete, budete schopni intuitivně dělat kompromisy a zaměřit své úsilí odpovídajícím způsobem. A stejně jako u žonglování je klíčem praxe.
Psaní vysoce výkonného kódu má vlastní trojici; Nastavení cílů, měření a porozumění cílové platformě Pokud nevíte, jak rychlý musí být váš kód, jak se dozvíte, až budete hotovi? Pokud kód neměříte a neprofilujete, jak poznáte, kdy jste dosáhli svých cílů nebo proč své cíle nesplňujete? Pokud nerozumíte platformě, na kterou cílíte, jak budete vědět, co optimalizovat v případě, že nesplníte své cíle. Tyto principy platí pro vývoj vysoce výkonného kódu obecně bez ohledu na to, na kterou platformu cílíte. Žádný článek o psaní vysoce výkonného kódu by nebyl úplný bez zmínky o této trojici. I když jsou všechny tři stejně významné, tento článek se zaměří na poslední dva aspekty, protože platí pro psaní vysoce výkonných aplikací, které cílí na rozhraní Microsoft® .NET Framework.
Základní principy psaní vysoce výkonného kódu na jakékoli platformě jsou:
- Nastavení cílů výkonu
- Změřit, změřit a pak ještě něco změřit
- Vysvětlení hardwarových a softwarových platforem, na které cílí vaše aplikace
Modul ClR (Common Language Runtime) rozhraní .NET
Jádrem rozhraní .NET Framework je modul CLR (Common Language Runtime). CLR poskytuje všechny služby modulu runtime pro váš kód. Kompilace za běhu, správa paměti, zabezpečení a řada dalších služeb. ClR byl navržen tak, aby byl vysoce výkonný. To znamená, že existují způsoby, jak můžete využít tento výkon a způsoby, jak ho můžete bránit.
Cílem tohoto článku je poskytnout přehled modulu Common Language Runtime z hlediska výkonu, identifikovat osvědčené postupy pro výkon spravovaného kódu a ukázat, jak můžete měřit výkon spravované aplikace. Tento článek není vyčerpávající diskuzí o charakteristikách výkonu rozhraní .NET Framework. Pro účely tohoto článku definujem výkon tak, aby zahrnoval propustnost, škálovatelnost, čas spuštění a využití paměti.
Spravovaná data a uvolňování paměti
Jedním z hlavních obav vývojářů při používání spravovaného kódu v aplikacích s kritickým výkonem jsou náklady na správu paměti modulu CLR, kterou provádí uvolňování paměti (GC). Náklady na správu paměti jsou funkcí nákladů na přidělení paměti přidružené k instanci typu, nákladů na správu této paměti po celou dobu životnosti instance a nákladů na uvolnění této paměti, když už není potřeba.
Spravovaná alokace je obvykle velmi levná; ve většině případů trvá méně času než C/C++ malloc
nebo new
. Důvodem je to, že CLR nemusí prohledávat volný seznam najít další dostupný souvislý blok paměti dostatečně velký, aby mohl nový objekt uložit; udržuje ukazatel na další volnou pozici v paměti. Přidělení spravované haldy si můžete představit jako "stack like". Přidělení může způsobit kolekci, pokud GC potřebuje uvolnit paměť pro přidělení nového objektu. V takovém případě je přidělení dražší než malloc
new
nebo . Připnuté objekty můžou také ovlivnit náklady na přidělení. Připnuté objekty jsou objekty, které byly pokynem, aby se během kolekce nepřesunuly, obvykle proto, že adresa objektu byla předána nativnímu rozhraní API.
malloc
new
Na rozdíl od nebo existují náklady spojené se správou paměti po celou dobu životnosti objektu. ClR GC je generační, což znamená, že se ne vždy shromažďuje celá halda. GC však stále potřebuje vědět, jestli nějaké živé objekty ve zbývajících kořenových objektech haldy v části haldy, která se shromažďuje. Správa paměti, která obsahuje objekty, které obsahují odkazy na objekty v mladších generacích, je po celou dobu životnosti objektů náročná.
GC je generační značka a uvolňování paměti. Spravovaná halda obsahuje tři generace; Generace 0 obsahuje všechny nové objekty, generace 1 obsahuje poněkud delší objekty a generace 2 obsahuje objekty s dlouhou životností. GC shromáždí nejmenší možnou část haldy, aby uvolnila dostatek paměti, aby aplikace pokračovala. Kolekce Generace zahrnuje kolekci všech mladších generací, v tomto případě kolekce Generace 1 také shromažďuje Generaci 0. Generace 0 má dynamickou velikost podle velikosti mezipaměti procesoru a alokační rychlosti aplikace a shromažďování obvykle trvá méně než 10 milisekund. Generace 1 má dynamickou velikost podle míry přidělování aplikace a shromažďování obvykle trvá 10 až 30 milisekund. Velikost generace 2 bude záviset na alokačním profilu vaší aplikace, stejně jako doba potřebná ke shromáždění. Právě tyto kolekce 2. generace mají největší vliv na výkon při správě paměti vašich aplikací.
NÁPOVĚDY GC se sám ladí a přizpůsobí se požadavkům na paměť aplikací. Ve většině případů bude ladění bránit vyvolání uvolňování paměti prostřednictvím kódu programu. "Pomáhá" uvolňování paměti voláním GC. Funkce Collect pravděpodobně nezlepší výkon vašich aplikací.
GC může během kolekce přemístit živé objekty. Pokud jsou tyto objekty velké, náklady na přemístění jsou vysoké, takže tyto objekty jsou přiděleny ve zvláštní oblasti haldy označované jako velká halda objektů. Halda velkých objektů se shromažďuje, ale není komprimovaná, například velké objekty se nepřemístí. Velké objekty jsou objekty, které jsou větší než 80 kB. Všimněte si, že v budoucích verzích CLR se to může změnit. Když je potřeba shromáždit haldu velkých objektů, vynutí úplnou kolekci a halda velkých objektů se shromažďuje během kolekcí Gen 2. Přidělení a míra smrti objektů v haldě velkých objektů může mít významný vliv na náklady na výkon při správě paměti aplikací.
Alokační profily
Celkový profil přidělení spravované aplikace definuje, jak tvrdě musí systém uvolňování paměti pracovat při správě paměti přidružené k aplikaci. Čím těžší je správa paměti, tím větší je počet cyklů procesoru, které GC trvá, a tím méně času procesoru stráví spouštěním kódu aplikace. Alokační profil je funkce počtu přidělených objektů, velikosti těchto objektů a jejich životnosti. Nejběžnějším způsobem, jak zmírnit tlak GC, je jednoduše přidělit méně objektů. Aplikace navržené pro rozšiřitelnost, modularitu a opakované použití pomocí technik návrhu orientovaného na objekty budou mít téměř vždy za následek zvýšený počet přidělení. Za abstrakci a "eleganci" existuje trest výkonu.
Alokační profil, který je vhodný pro uvolňování paměti, bude mít na začátku aplikace přidělené některé objekty, které pak přežívají po celou dobu životnosti aplikace, a všechny ostatní objekty budou krátkodobé. Dlouhodobé objekty budou obsahovat jen málo nebo žádné odkazy na krátkodobé objekty. Vzhledem k tomu, že se od toho liší alokační profil, bude muset GC na správě paměti aplikací pracovat více.
Alokační profil GC-unfriendly bude mít mnoho objektů, které přežívají do generace 2 a pak zaniknou, nebo bude mít mnoho krátkodobých objektů přidělených ve velké haldě objektů. Objekty, které přežijí dostatečně dlouho, aby se dostaly do generace 2 a pak zemřely, jsou nejdražší na správu. Jak jsem zmínil dříve objekty ve starších generacích, které obsahují odkazy na objekty v mladších generacích během GC, také zvýšit náklady na kolekci.
Typický reálný alokační profil bude někde mezi dvěma výše uvedenými alokačními profily. Důležitou metrikou vašeho alokačního profilu je procento celkového času procesoru, který se stráví v uvolňování paměti. Toto číslo můžete získat z čítače výkonu paměti .NET CLR: % času v čítači výkonu GC. Pokud je střední hodnota tohoto čítače vyšší než 30 %, měli byste zvážit podrobnější pohled na váš alokační profil. To nemusí nutně znamenat, že váš alokační profil je "špatný"; existuje několik aplikací náročných na paměť, u kterých je tato úroveň uvolňování paměti nezbytná a vhodná. Tento čítač by měl být první věc, na kterou se podíváte, pokud narazíte na problémy s výkonem; mělo by se okamžitě zobrazit, jestli je váš alokační profil součástí problému.
NÁPOVĚDY Pokud hodnota .NET CLR Memory: % Time in Gc performance čítač indikuje, že vaše aplikace tráví v uvolňování paměti průměrně více než 30 % času, měli byste se blíže podívat na alokační profil.
NÁPOVĚDY Aplikace, která je vhodná pro uvolňování paměti, bude mít výrazně více kolekcí generace 0 než kolekce 2. generace. Tento poměr lze zjistit porovnáním čítačů výkonu net CLR Memory: # Gen 0 Collections a NET CLR Memory: # Gen2 Collections.
Rozhraní API pro profilaci a profiler CLR
CLR obsahuje výkonné rozhraní API pro profilaci, které umožňuje 3. stranám psát vlastní profilátory pro spravované aplikace. ClR Profiler je nepodporovaný ukázkový nástroj pro profilaci přidělení, který napsal produktový tým CLR a který používá toto rozhraní API pro profilaci. ClR Profiler umožňuje vývojářům zobrazit alokační profil jejich aplikací pro správu.
Obrázek 1 – hlavní okno profileru CLR
Profiler CLR obsahuje řadu velmi užitečných zobrazení profilu přidělení, včetně histogramu přidělených typů, grafů přidělení a volání, časové čáry znázorňující GC různých generací a výsledného stavu spravované haldy po těchto kolekcích a stromu volání zobrazující přidělování podle metod a načtení sestavení.
Obrázek 2 Graf přidělení profileru CLR
NÁPOVĚDY Podrobnosti o tom, jak používat CLR Profiler, najdete v souboru readme, který je součástí souboru zip.
Všimněte si, že profiler CLR má vysokou režii a výrazně mění charakteristiky výkonu vaší aplikace. Vznikající stresové chyby pravděpodobně zmizí, když spustíte aplikaci s profilerem CLR.
Hostování serveru GC
Pro CLR jsou k dispozici dva různé uvolňování paměti: GC pracovní stanice a Server GC. Konzolové a model Windows Forms aplikace hostují GC pracovní stanice a ASP.NET hostuje Server GC. Server GC je optimalizovaný pro propustnost a škálovatelnost s více procesory. GC serveru pozastaví všechna vlákna se spuštěným spravovaným kódem po celou dobu trvání kolekce, včetně fází Mark i Sweep, a uvolňování paměti probíhá paralelně na všech procesorech dostupných pro proces na vyhrazených vláknech s vysokou prioritou spřažených s procesorem. Pokud vlákna spouští nativní kód během uvolňování paměti, jsou tato vlákna pozastavena pouze v případě, že se nativní volání vrátí. Pokud vytváříte serverovou aplikaci, která bude běžet na víceprocesorových počítačích, důrazně doporučujeme použít serverový GC. Pokud vaše aplikace není hostovaná službou ASP.NET, budete muset napsat nativní aplikaci, která explicitně hostuje CLR.
NÁPOVĚDY Pokud vytváříte škálovatelné serverové aplikace, hostujte Server GC. Viz Implementace vlastního hostitele modulu Common Language Runtime pro spravovanou aplikaci.
Pracovní stanice GC je optimalizovaná pro nízkou latenci, která se obvykle vyžaduje pro klientské aplikace. Jeden nechce, aby během uvolňování paměti došlo k patrnému pozastavení klientské aplikace, protože výkon klienta se obvykle neměří podle nezpracované propustnosti, ale spíše podle vnímaný výkon. Pracovní stanice GC provádí souběžný uvolňování paměti, což znamená, že provádí fázi označení, zatímco spravovaný kód je stále spuštěný. Uvolňování paměti pozastaví pouze vlákna, na kterých běží spravovaný kód, když potřebuje provést fázi úklidu. V GC pracovní stanice se uvolňování paměti provádí pouze na jednom vlákně, a proto pouze na jednom procesoru.
Dokončení
CLR poskytuje mechanismus, kdy se čištění provádí automaticky před uvolněním paměti přidružené k instanci typu. Tento mechanismus se nazývá finalizace. Finalizace se obvykle používá k uvolnění nativních prostředků, v tomto případě databázová připojení nebo popisovačů operačního systému, které jsou používány objektem.
Finalizace je nákladná funkce a zvyšuje tlak, který je kladen na uvolňování paměti. GC sleduje objekty, které vyžadují finalizaci ve frontě s možností dokončení. Pokud během kolekce nástroj GC najde objekt, který už není aktivní, ale vyžaduje dokončení, pak se položka tohoto objektu ve frontě s možností dokončení přesune do fronty FReachable. Finalizace probíhá v samostatném vlákně s názvem Vlákno finalizátoru. Vzhledem k tomu, že při provádění finalizátoru může být vyžadován celý stav objektu, objekt a všechny objekty, na které odkazuje, jsou povýšeny na další generaci. Paměť přidružená k objektu nebo grafu objektů se uvolní pouze během následujícího uvolňování paměti.
Prostředky, které je třeba uvolnit, by měly být zabaleny do co nejmenšího dokončitelného objektu; Pokud například vaše třída vyžaduje odkazy na spravované i nespravované prostředky, měli byste nespravované prostředky zabalit do nové třídy Finalizable a nastavit tuto třídu jako člena vaší třídy. Nadřazená třída by neměla být finalizovatelná. To znamená, že se zvýší pouze třída, která obsahuje nespravované prostředky (za předpokladu, že nemáte odkaz na nadřazenou třídu ve třídě obsahující nespravované prostředky). Další věc, kterou je třeba mít na paměti, je, že existuje pouze jedno vlákno finalizace. Pokud finalizátor způsobí blokování tohoto vlákna, následné finalizátory nebudou volány, prostředky nebudou uvolněny a aplikace nevracela.
NÁPOVĚDY Finalizační metody by měly být co nejjednodušší a nikdy by neměly blokovat.
NÁPOVĚDY Umožňuje dokončit pouze třídu obálky kolem nespravovaných objektů, které potřebují vyčistit.
Dokončení si můžete představit jako alternativu k počítání odkazů. Objekt, který implementuje počítání odkazů, sleduje, kolik dalších objektů na něj odkazuje (což může vést k některým velmi dobře známým problémům), aby mohl uvolnit své prostředky, když je počet odkazů nulový. Modul CLR neimplementuje počítání odkazů, takže potřebuje poskytnout mechanismus pro automatické uvolnění prostředků, když se žádné další odkazy na objekt neudržují. Finalizace je tento mechanismus. Dokončení je obvykle nutné pouze v případě, že životnost objektu, který vyžaduje vyčištění, není explicitně známa.
Model Dispose
V případě, že životnost objektu je explicitně známa, nespravované prostředky přidružené k objektu by měly být dychtivě uvolněny. Tomu se říká "Likvidace" objektu. Model Dispose se implementuje prostřednictvím rozhraní IDisposable (i když jeho implementace by byla triviální). Pokud chcete pro vaši třídu zpřístupnit nedočkavé finalizace, například zpřístupnit instance vaší třídy k dispozici, potřebujete, aby objekt implementovali rozhraní IDisposable a poskytli implementaci pro metodu Dispose . V metodě Dispose budete volat stejný kód čištění, který je v Finalizer a informovat GC, že již nemusí dokončit objekt voláním GC. Metoda SuppressFinalization . Je vhodné, aby metoda Dispose i finalizer volala společnou finalizační funkci, aby bylo nutné udržovat pouze jednu verzi kódu pro čištění. Také, pokud sémantiky objektu jsou takové, že Close metoda bude logičtější než Dispose metoda pak close by také měl být implementován; v tomto případě je připojení k databázi nebo soket logicky "uzavřeno". Close může jednoduše volat Metodu Dispose.
Vždy je dobrým postupem poskytnout metodu Dispose pro třídy s finalizátorem; nikdy si nemůžeme být jistí, jak bude tato třída použita, například jestli bude její životnost explicitně známa nebo ne. Pokud třída, kterou používáte, implementuje model Dispose a vy explicitně víte, kdy jste s objektem hotovi, určitě volejte Dispose.
NÁPOVĚDY Zadejte metodu Dispose pro všechny třídy, které lze dokončit.
NÁPOVĚDY Potlačit finalization v metodě Dispose .
NÁPOVĚDY Volejte běžnou funkci čištění.
NÁPOVĚDY Pokud objekt, který používáte, implementuje IDisposable a víte, že objekt už není potřeba, zavolejte Dispose.
Jazyk C# poskytuje velmi pohodlný způsob, jak automaticky odstranit objekty. Klíčové using
slovo umožňuje identifikovat blok kódu, po kterém bude dispose volána na řadě jednorázových objektů.
Klíčové slovo v jazyce C#
using(DisposableType T)
{
//Do some work with T
}
//T.Dispose() is called automatically
Poznámka k slabým odkazům
Jakýkoli odkaz na objekt, který je v zásobníku, v registru, v jiném objektu nebo v jednom z ostatních kořenů GC, zachová objekt při uvolňování paměti. To je obvykle velmi dobrá věc, vzhledem k tomu, že to obvykle znamená, že vaše aplikace není s tímto objektem hotová. Existují však případy, kdy chcete mít odkaz na objekt, ale nechcete ovlivnit jeho životnost. V těchto případech CLR poskytuje mechanismus označovaný jako slabé odkazy, který právě k tomu slouží. Jakýkoli silný odkaz – například odkaz, který odsadí objekt – může být převeden na slabý odkaz. Příkladem použití slabých odkazů je, když chcete vytvořit objekt externího kurzoru, který může procházet datovou strukturou, ale neměl by mít vliv na životnost objektu. Dalším příkladem je, pokud chcete vytvořit mezipaměť, která je vyprázdněna při zatížení paměti; například když dojde k uvolňování paměti.
Vytvoření slabého odkazu v jazyce C#
MyRefType mrt = new MyRefType();
//...
//Create weak reference
WeakReference wr = new WeakReference(mrt);
mrt = null; //object is no longer rooted
//...
//Has object been collected?
if(wr.IsAlive)
{
//Get a strong reference to the object
mrt = wr.Target;
//object is rooted and can be used again
}
else
{
//recreate the object
mrt = new MyRefType();
}
Spravovaný kód a JIT CLR
Spravovaná sestavení, která jsou jednotkou distribuce spravovaného kódu, obsahují jazyk nezávislý na procesoru s názvem Microsoft Intermediate Language (MSIL nebo IL). ClR Just-In-Time (JIT) kompiluje il do optimalizovaných nativních instrukcí X86. JIT je optimalizační kompilátor, ale protože kompilace probíhá za běhu a pouze při prvním zavolání metody, musí být počet optimalizací, které provádí, vyvážen s časem, který trvá kompilace. Obvykle to není důležité pro serverové aplikace, protože čas spuštění a rychlost odezvy obvykle není problém, ale je to důležité pro klientské aplikace. Všimněte si, že čas spuštění lze zlepšit kompilací v době instalace pomocí NGEN.exe.
Mnoho optimalizací, které provádí JIT, nemá přidružené programové vzory, například pro ně nemůžete explicitně kódovat, ale existuje počet, které ano. Další část popisuje některé z těchto optimalizací.
NÁPOVĚDY Zlepšete dobu spouštění klientských aplikací kompilací aplikace v době instalace pomocí nástroje NGEN.exe.
Vkládání metod
S voláním metody jsou spojené náklady; argumenty musí být vloženy do zásobníku nebo uloženy v registrech, je třeba spustit metodu prolog a epilog atd. Náklady na tato volání lze u některých metod zabránit jednoduchým přesunutím těla metody volané metody do těla volajícího. Tato metoda se nazývá vložka metody. JIT používá řadu heuristiky k rozhodnutí, zda má být metoda vložena do řádku. Následuje seznam významnějších z nich (všimněte si, že to není vyčerpávající):
- Metody, které jsou větší než 32 bajtů IL, nebudou vloženy.
- Virtuální funkce nejsou vložené.
- Metody, které mají komplexní řízení toku, nebudou vloženy. Komplexní řízení toku je jakékoli jiné řízení toku než
if/then/else;
v tomto případě neboswitch
while
. - Metody, které obsahují bloky zpracování výjimek, nejsou vloženy, i když metody, které vyvolávají výjimky, jsou stále kandidáty na vkládání.
- Pokud některý z formálních argumentů metody jsou struktury, metoda nebude vložena.
Pečlivě bych zvážil explicitní kódování pro tyto heuristiky, protože by se mohly v budoucích verzích JIT změnit. Neohrožujte správnost metody a pokuste se zaručit, že bude vložena. Je zajímavé poznamenat, že inline
klíčová slova a __inline
v jazyce C++ nezaručují, že kompilátor bude vložen metodu (i když __forceinline
ano).
Metody získání a nastavení vlastností jsou obecně vhodnými kandidáty pro vkládání, protože obvykle slouží k inicializaci soukromých datových členů.
**HINT **Neohrožujte správnost metody při pokusu o zaručení inliningu.
Vyloučení kontroly rozsahu
Jednou z mnoha výhod spravovaného kódu je automatická kontrola rozsahu. při každém přístupu k poli pomocí sémantiky array[index] jiT vygeneruje kontrolu, která zajistí, že je index v mezích pole. V kontextu smyček s velkým počtem iterací a malým počtem pokynů spuštěných na iteraci mohou být tyto kontroly rozsahu nákladné. Existují případy, kdy JIT zjistí, že tyto kontroly rozsahu nejsou nutné, a odstraní kontrolu z těla smyčky, a to pouze jednou před zahájením provádění smyčky. V jazyce C# existuje programový vzor, který zajišťuje, že budou tyto kontroly rozsahu eliminovány: explicitně otestujte délku pole v příkazu "for". Všimněte si, že drobné odchylky od tohoto vzoru způsobí, že se kontrola neodstraní a v tomto případě přidá hodnotu do indexu.
Vyloučení kontroly rozsahu v C#
//Range check will be eliminated
for(int i = 0; i < myArray.Length; i++)
{
Console.WriteLine(myArray[i].ToString());
}
//Range check will NOT be eliminated
for(int i = 0; i < myArray.Length + y; i++)
{
Console.WriteLine(myArray[i+x].ToString());
}
Optimalizace je obzvláště patrná při vyhledávání velkých zubatých polí, například při vyloučení kontroly rozsahu vnitřní i vnější smyčky.
Optimalizace, které vyžadují sledování využití proměnných
Počet optimalizací kompilátoru JIT vyžaduje, aby JIT sledoval použití formálních argumentů a místních proměnných. například kdy jsou poprvé použity a kdy naposledy jsou použity v těle metody. Ve verzi 1.0 a 1.1 modulu CLR platí omezení 64 pro celkový počet proměnných, pro které bude JIT sledovat využití. Příkladem optimalizace, která vyžaduje sledování využití, je Enregistration. Registrace je, když jsou proměnné uloženy v registrech procesoru, nikoli v rámci zásobníku, například v paměti RAM. Přístup k proměnným enregistered je výrazně rychlejší než v případě, že jsou v rámci zásobníku, a to i v případě, že proměnná na snímku je v mezipaměti procesoru. Pouze 64 proměnných bude považováno za enregistration; všechny ostatní proměnné budou nasdílené do zásobníku. Existují i jiné optimalizace než Enregistration, které závisí na sledování využití. Počet formálních argumentů a místních hodnot pro metodu by měl být nižší než 64, aby byl zajištěn maximální počet optimalizací JIT. Mějte na paměti, že toto číslo se může pro budoucí verze CLR změnit.
NÁPOVĚDY Udržujte metody krátké. Má to několik důvodů, včetně vkládání metod, enregistration a doba trvání JIT.
Další optimalizace JIT
Kompilátor JIT dělá řadu dalších optimalizací: šíření konstant a kopírování, invariantní zdvihání smyčky a několik dalších. Neexistují žádné explicitní programovací vzory, které je nutné použít k získání těchto optimalizací; jsou svobodní.
Proč se tyto optimalizace nezobrazují v sadě Visual Studio?
Pokud použijete nabídku Spustit z nabídky Ladění nebo stisknutím klávesy F5 spustíte aplikaci v sadě Visual Studio bez ohledu na to, jestli jste vytvořili vydanou nebo ladicí verzi, všechny optimalizace JIT budou zakázány. Když je spravovaná aplikace spuštěna ladicí program, i když se nejedná o ladicí sestavení aplikace, JIT vygeneruje neoptimalizovat instrukce x86. Pokud chcete, aby JIT vygeneroval optimalizovaný kód, spusťte aplikaci z Průzkumníka Windows nebo v sadě Visual Studio použijte kombinaci kláves CTRL+F5. Pokud chcete zobrazit optimalizovanou demontáž a kontrastovat s neoptimalizovatelným kódem, můžete použít cordbg.exe.
NÁPOVĚDY Pomocí cordbg.exe můžete zobrazit jak optimalizovaný, tak neoptimalizovatelný kód vygenerovaný jitem. Po spuštění aplikace s cordbg.exe můžete nastavit režim JIT zadáním následujícího příkazu:
(cordbg) mode JitOptimizations 1
JIT's will produce optimized code
(cordbg) mode JitOptimizations 0
JiT vytvoří laditelný (neoptimalizovatelný) kód.
Typy hodnot
CLR zveřejňuje dvě různé sady typů, odkazové typy a hodnotové typy. Typy odkazů se vždy přidělují na spravované haldě a předávají se odkazem (jak název napovídá). Typy hodnot se přidělují v zásobníku nebo vložené jako součást objektu na haldě a ve výchozím nastavení se předávají podle hodnoty, ale můžete je také předat odkazem. Typy hodnot se přidělují velmi levně a za předpokladu, že jsou malé a jednoduché, je levné je předat jako argumenty. Dobrým příkladem vhodného použití hodnotových typů by byl bodový typ hodnoty, který obsahuje souřadnici x a y .
Typ hodnoty bodu
struct Point
{
public int x;
public int y;
//
}
Hodnotové typy lze také považovat za objekty; Na nich lze například volat metody objektů, lze je přetypovat na objekt nebo předat tam, kde je objekt očekávána. Když k tomu dojde, typ hodnoty je převeden na typ odkazu prostřednictvím procesu s názvem Boxing. Pokud je typ hodnoty Boxed, je na spravované haldě přidělen nový objekt a hodnota se zkopíruje do nového objektu. Jedná se o nákladnou operaci, která může snížit nebo zcela negovat výkon získaný pomocí hodnotových typů. Pokud je typ Boxed implicitně nebo explicitně přetypován zpět na typ hodnoty, je unboxed.
Typ hodnoty pole/unboxu
C#:
int BoxUnboxValueType()
{
int i = 10;
object o = (object)i; //i is Boxed
return (int)o + 3; //i is Unboxed
}
MSIL:
.method private hidebysig instance int32
BoxUnboxValueType() cil managed
{
// Code size 20 (0x14)
.maxstack 2
.locals init (int32 V_0,
object V_1)
IL_0000: ldc.i4.s 10
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: box [mscorlib]System.Int32
IL_0009: stloc.1
IL_000a: ldloc.1
IL_000b: unbox [mscorlib]System.Int32
IL_0010: ldind.i4
IL_0011: ldc.i4.3
IL_0012: add
IL_0013: ret
} // end of method Class1::BoxUnboxValueType
Pokud implementujete vlastní hodnotové typy (struktura v jazyce C#), měli byste zvážit přepsání toString metody. Pokud tuto metodu nepřepíšete, volání ToString na typ hodnoty způsobí, že typ bude Boxed. To platí i pro ostatní metody, které jsou zděděny z System.Object, v tomto případě Equals, ačkoli ToString je pravděpodobně nejčastěji nazývaná metoda. Pokud chcete zjistit, jestli a kdy je váš typ hodnoty boxován, můžete pomocí nástroje ildasm.exe (jako ve fragmentu kódu výše) vyhledat box
pokyny v knihovně MSIL.
Přepsání metody ToString() v jazyce C# za účelem zabránění balení
struct Point
{
public int x;
public int y;
//This will prevent type being boxed when ToString is called
public override string ToString()
{
return x.ToString() + "," + y.ToString();
}
}
Mějte na paměti, že při vytváření kolekcí (například ArrayList s plovoucím polem) se při přidání do kolekce zobrazí každá položka. Měli byste zvážit použití pole nebo vytvoření vlastní třídy kolekce pro váš typ hodnoty.
Implicitní balení při použití tříd kolekcí v jazyce C#
ArrayList al = new ArrayList();
al.Add(42.0F); //Implicitly Boxed becuase Add() takes object
float f = (float)al[0]; //Unboxed
Zpracování výjimek
Běžné je používat chybové stavy jako normální řízení toku. V tomto případě se při pokusu o programové přidání uživatele do instance služby Active Directory můžete jednoduše pokusit přidat uživatele, a pokud se vrátí E_ADS_OBJECT_EXISTS HRESULT, víte, že již v adresáři existují. Případně můžete vyhledat uživatele v adresáři a pak uživatele přidat jenom v případě, že hledání selže.
Toto použití chyb pro normální řízení toku je vzorem výkonu v kontextu CLR. Zpracování chyb v CLR se provádí se strukturovaným zpracováním výjimek. Spravované výjimky jsou velmi levné, dokud je nevyhodíte. V modulu CLR se při vyvolání výjimky vyžaduje procházení zásobníku, aby se našla odpovídající obslužná rutina výjimky pro vyvolánou výjimku. Stack walking je nákladná operace. Výjimky by měly být použity, jak jejich název napovídá; za výjimečných nebo neočekávaných okolností.
**HINT **Zvažte vrácení výčtu výsledků pro očekávané výsledky namísto vyvolání výjimky pro metody důležité pro výkon.
**HINT **Existuje několik čítačů výkonu výjimek .NET CLR, které vám řeknou, kolik výjimek je vyvolána ve vaší aplikaci.
**HINT **Pokud používáte VB.NET místo
On Error Goto
; objekt chyby představuje zbytečné náklady.
Vytváření vláken a synchronizace
MODUL CLR zpřístupňuje bohaté funkce pro vytváření vláken a synchronizaci, včetně možnosti vytvářet vlastní vlákna, fond vláken a různé primitivy synchronizace. Než využijete podporu threadingu v CLR, měli byste pečlivě zvážit použití vláken. Mějte na paměti, že přidání vláken může ve skutečnosti snížit propustnost, nikoli ji zvýšit, a můžete si být jisti, že zvýší vaše využití paměti. V serverových aplikacích, které budou běžet na počítačích s více procesory, může přidání vláken výrazně zlepšit propustnost paralelizací provádění (i když závisí na tom, kolik kolizí zámků probíhá, například serializace spouštění), a v klientských aplikacích může přidání vlákna pro zobrazení aktivity nebo průběhu zlepšit vnímaný výkon (při malých nákladech na propustnost).
Pokud vlákna ve vaší aplikaci nejsou specializovaná na konkrétní úlohu nebo mají přidružený zvláštní stav, měli byste zvážit použití fondu vláken. Pokud jste v minulosti používali fond vláken Win32, bude vám fond vláken CLR velmi povědomý. Pro každý spravovaný proces existuje jedna instance fondu vláken. Fond vláken je inteligentní ohledně počtu vláken, která vytvoří, a bude se ladit podle zatížení počítače.
Bez diskuze o synchronizaci nelze diskutovat o vláknech. Všechny zvýšení propustnosti, které může vícevláknové využití vaší aplikaci poskytnout, může být negováno špatně napsanou logikou synchronizace. Členitost zámků může výrazně ovlivnit celkovou propustnost vaší aplikace, a to jak kvůli nákladům na vytváření a správu zámku, tak i kvůli tomu, že zámky můžou potenciálně serializovat provádění. K ilustraci tohoto bodu použijeme příklad pokusu o přidání uzlu do stromu. Pokud má být strom sdílenou datovou strukturou, potřebuje k němu během provádění aplikace přístup více vláken a vy budete muset synchronizovat přístup ke stromu. Při přidávání uzlu můžete zamknout celý strom, což znamená, že se vám účtují pouze náklady na vytvoření jednoho zámku, ale ostatní vlákna, která se pokoušejí o přístup ke stromu, se pravděpodobně zablokují. Toto by byl příklad hrubého zámku. Alternativně můžete uzamknout každý uzel při procházení stromu, což by znamenalo náklady na vytvoření zámku pro každý uzel, ale ostatní vlákna by se neblokovala, pokud se nepokusila o přístup ke konkrétnímu uzlu, který jste uzamkli. Toto je příklad jemného zámku. Pravděpodobně vhodnější členitost zámku by bylo uzamknout pouze pod strom, se kterým pracujete. Všimněte si, že v tomto příkladu byste pravděpodobně použili sdílený zámek (RWLock), protože přístup by mělo mít více čtenářů najednou.
Nejjednodušším a nejvýkonnějším způsobem provádění synchronizovaných operací je použití třídy System.Threading.Interlocked. Interlocked Třída zveřejňuje řadu atomických operací nízké úrovně: Increment, Dekrement, Exchange a CompareExchange.
Použití třídy System.Threading.Interlocked v jazyce C#
using System.Threading;
//...
public class MyClass
{
void MyClass() //Constructor
{
//Increment a global instance counter atomically
Interlocked.Increment(ref MyClassInstanceCounter);
}
~MyClass() //Finalizer
{
//Decrement a global instance counter atomically
Interlocked.Decrement(ref MyClassInstanceCounter);
//...
}
//...
}
Pravděpodobně nejčastěji používaným synchronizačním mechanismem je monitorování nebo kritická část. Zámek monitoru lze použít přímo nebo pomocí klíčového lock
slova v jazyce C#. Klíčové lock
slovo synchronizuje přístup pro daný objekt do konkrétního bloku kódu. Zámek monitoru, který je poměrně lehce napadnut, je relativně levný z hlediska výkonu, ale je dražší, pokud je vysoce sporný.
Klíčové slovo C# lock
//Thread will attempt to obtain the lock
//and block until it does
lock(mySharedObject)
{
//A thread will only be able to execute the code
//within this block if it holds the lock
}//Thread releases the lock
RWLock poskytuje sdílený zamykací mechanismus: například čtenáři můžou sdílet zámek s jinými čtenáři, ale zapisovač nemůže. V případech, kdy je to možné, může RWLock vést k lepší propustnosti než použití monitoru, což by umožnilo získat zámek najednou pouze jednomu čtenáři nebo zapisovači. Obor názvů System.Threading obsahuje také třídu Mutex. Mutex je primitivo synchronizace, které umožňuje synchronizaci mezi procesy. Mějte na paměti, že je to výrazně dražší než kritická část a měla by se používat pouze v případě, že je vyžadována synchronizace mezi procesy.
Reflexe
Reflexe je mechanismus poskytovaný modulem CLR, který umožňuje získat informace o typu programově za běhu. Reflexe závisí do značné míry na metadatech, která jsou vložena ve spravovaných sestaveních. Mnoho rozhraní API reflexe vyžaduje vyhledávání a analýzu metadat, což jsou nákladné operace.
Rozhraní API reflexe lze seskupit do tří výkonnostních kontejnerů. porovnání typů, výčet členů a vyvolání členů. Každý z těchto kbelíků se postupně zdražuje. Operace porovnání typů – v tomto případě typeof v jazyce C#, GetType, is, IsInstanceOfType atd.) jsou nejlevnější z rozhraní API reflexe, i když nejsou v žádném případě levné. Výčty členů umožňují programově kontrolovat metody, vlastnosti, pole, události, konstruktory a tak dále třídy. Příkladem toho, kde mohou být použity, je ve scénářích návrhu, v tomto případě výčet vlastností celních webových ovládacích prvků pro prohlížeč vlastností v sadě Visual Studio. Nejdražší z rozhraní API reflexe jsou ta, která umožňují dynamicky vyvolat členy třídy nebo dynamicky generovat JIT a spouštět metodu. Určitě existují scénáře s pozdní vazbou, kdy se vyžaduje dynamické načítání sestavení, vytváření instancí typů a volání metod, ale toto volné spojení vyžaduje explicitní kompromis mezi výkonem. Obecně platí, že v cestách kódu citlivých na výkon byste se měli vyhnout rozhraním API pro reflexi. Mějte na paměti, že i když reflexi přímo nepoužíváte, rozhraní API, které používáte, ho může používat. Proto také mějte na paměti přechodné použití rozhraní API reflexe.
Pozdní vazba
Volání s pozdní vazbou jsou příkladem funkce, která pod pozadím používá reflexi. Vizuální Basic.NET i JScript.NET podporují volání s pozdní vazbou. Například před jejím použitím nemusíte deklarovat proměnnou. Objekty s pozdní vazbou jsou ve skutečnosti typu objekt a reflexe se používá k převodu objektu na správný typ za běhu. Volání s pozdní vazbou je o řád pomalejší než přímé volání. Pokud výslovně nepotřebujete chování s pozdní vazbou, měli byste se vyhnout jeho použití v cestách kódu kritických pro výkon.
NÁPOVĚDY Pokud používáte VB.NET a explicitně nepotřebujete pozdní vazbu, můžete kompilátoru říct, aby ji nepovolil zahrnutím
Option Explicit On
aOption Strict On
na začátek zdrojových souborů. Tyto možnosti vás vynutí deklarovat a silně zadávat proměnné a vypínat implicitní přetypování.
Zabezpečení
Zabezpečení je nezbytnou a nedílnou součástí CLR a má spojené náklady na výkon. V případě, že je kód plně důvěryhodný a zásady zabezpečení jsou výchozí, zabezpečení by mělo mít malý vliv na propustnost a dobu spuštění aplikace. Částečně důvěryhodný kód – například kód ze zóny internetu nebo intranetu – nebo zúžení sady grantů MyComputer zvýší náklady na výkon zabezpečení.
Volání zprostředkovatele komunikace a platformy modelu COM
Volání zprostředkovatele komunikace a platformy modelu COM zpřístupňuje nativní rozhraní API spravovanému kódu téměř transparentním způsobem; Volání většiny nativních rozhraní API obvykle nevyžaduje žádný speciální kód, i když může vyžadovat několik kliknutí myší. Jak můžete očekávat, s voláním nativního kódu ze spravovaného kódu jsou spojené náklady a naopak. Tyto náklady mají dvě komponenty: pevné náklady spojené s přechody mezi nativním a spravovaným kódem a variabilní náklady spojené s případným zařazováním argumentů a návratových hodnot, které můžou být potřeba. Pevný příspěvek k nákladům na zprostředkovatele komunikace com i volání je malý: obvykle méně než 50 instrukcí. Náklady na zařazování do spravovaných typů a z těchto typů budou záviset na tom, jak se reprezentace liší na obou stranách hranice. Typy, které vyžadují významnou část transformace, budou dražší. Například všechny řetězce v CLR jsou řetězce Unicode. Pokud voláte rozhraní API Win32 prostřednictvím volání P/Invoke, které očekává pole znaků ANSI, je nutné zúžit každý znak v řetězci. Pokud je však spravované celočíselné pole předáno tam, kde se očekává nativní celočíselné pole, není zařazování vyžadováno.
Vzhledem k tomu, že volání nativního kódu souvisí s náklady na výkon, měli byste se ujistit, že jsou náklady oprávněné. Pokud se chystáte provést nativní hovor, ujistěte se, že práce, kterou nativní volání provádí, ospravedlňuje náklady na výkon spojené s voláním – ponechte metody "chunky" místo "chatty". Dobrým způsobem, jak změřit náklady na nativní volání, je změřit výkon nativní metody, která nepřijímá žádné argumenty a nemá žádnou návratovou hodnotu, a pak změřit výkon nativní metody, kterou chcete volat. Rozdíl vám poskytne informace o nákladech na zařazování.
NÁPOVĚDY Proveďte "chunky" volání zprostředkovatele komunikace COM a volání P/Invoke namísto "Chatty" volání a ujistěte se, že náklady na volání jsou odůvodněny množstvím práce, kterou volání provádí.
Všimněte si, že ke spravovaným vláknům nejsou přidruženy žádné modely podprocesů. Když se chystáte provést volání zprostředkovatele komunikace COM, musíte se ujistit, že vlákno, na které bude volání provedeno, je inicializováno na správný model vláken modelu COM. To se obvykle provádí pomocí MTAThreadAttribute a STAThreadAttribute (i když to lze provést také programově).
Čítače výkonu
Pro .NET CLR je zpřístupněna řada čítačů výkonu Windows. Tyto čítače výkonu by měly být zvolenou zbraní vývojáře při první diagnostice problému s výkonem nebo při pokusu o identifikaci charakteristik výkonu spravované aplikace. Již jsem zmínil několik čítačů, které se týkají správy paměti a výjimek. Existují čítače výkonu pro téměř všechny aspekty modulu CLR a rozhraní .NET Framework. Tyto čítače výkonu jsou vždy k dispozici a jsou neinvazivní; mají nízkou režii a nemění charakteristiky výkonu vaší aplikace.
Další nástroje
Kromě čítačů výkonu a profileru CLR budete chtít pomocí konvenčního profileru zjistit, které metody ve vaší aplikaci zabírají nejvíce času a které se volají nejčastěji. To budou metody, které optimalizujete jako první. K dispozici je řada komerčních profilátorů, které podporují spravovaný kód, včetně DevPartner Studio Professional Edition 7.0 od společnosti Compuware a VTune™ Analyzátor výkonu 7.0 od společnosti Intel®. Compuware také vytvoří bezplatný profiler pro spravovaný kód s názvem DevPartner Profiler Community Edition.
Závěr
Tento článek právě začíná zkoumání modulu CLR a rozhraní .NET Framework z hlediska výkonu. Existuje mnoho dalších aspektů architektury modulu CLR a rozhraní .NET Framework, které ovlivní výkon vaší aplikace. Nejlepším návodem, který mohu poskytnout každému vývojáři, je nedávat žádné předpoklady o výkonu platformy, na kterou vaše aplikace cílí, a rozhraní API, která používáte. Změřit všechno!
Šťastné žonglování.