Důležité informace o výkonu pro technologie Run-Time v rozhraní .NET Framework
Emanuel Schanzer
Microsoft Corporation
Dne
Shrnutí: Tento článek obsahuje průzkum různých technologií, které fungují ve spravovaném světě, a technické vysvětlení jejich vlivu na výkon. Seznamte se s fungováním uvolňování paměti, JIT, vzdálené komunikace, ValueTypes, zabezpečením a dalšími funkcemi. (27 tištěných stránek)
Obsah
Přehled
Kolekce paměti
Fond vláken
The JIT
AppDomains
Zabezpečení
Remoting
Typy hodnot
Další materiály
Příloha: Hostování doby běhu serveru
Přehled
Doba běhu .NET zavádí několik pokročilých technologií zaměřených na zabezpečení, usnadnění vývoje a výkon. Jako vývojář je důležité porozumět jednotlivým technologiím a efektivně je používat v kódu. Pokročilé nástroje poskytované v době běhu usnadňují vytvoření robustní aplikace, ale zajištění rychlého letu aplikace je (a vždy bylo) zodpovědností vývojáře.
Tento dokument white paper by vám měl poskytnout hlubší porozumění technologiím, které v .NET pracují, a měl by vám pomoct vyladit kód pro rychlost. Poznámka: Toto není list specifikací. Existuje spousta solidních technických informací. Cílem je poskytnout informace se silným sklonem k výkonu a nemusí zodpovědět všechny technické otázky. Pokud nenajdete odpovědi, které hledáte tady, doporučujeme projít si další informace v online knihovně MSDN.
Chystám se probrat následující technologie a poskytnout základní přehled o jejich účelu a důvodech, proč ovlivňují výkon. Pak se ponořím do podrobností implementace na nižší úrovni a použiju vzorový kód k ilustraci způsobů, jak z jednotlivých technologií rychle vyjít.
Kolekce paměti
Základy
Uvolňování paměti (GC) osvobodí programátora od běžných a obtížně ladit chyb tím, že uvolní paměť pro objekty, které se již nepoužívají. Obecná cesta po dobu životnosti objektu je následující ve spravovaném i nativním kódu:
Foo a = new Foo(); // Allocate memory for the object and initialize
...a... // Use the object
delete a; // Tear down the state of the object, clean up
// and free the memory for that object
V nativním kódu musíte všechno udělat sami. Chybějící fáze přidělení nebo vyčištění může mít za následek zcela nepředvídatelné chování, které je obtížné ladit, a zapomenutí volných objektů může vést k nevrácení paměti. Cesta pro přidělení paměti v modulu CLR (Common Language Runtime) je velmi blízko cestě, kterou jsme právě probrali. Když přidáme informace specifické pro uvolňování paměti, bude to vypadat velmi podobně.
Foo a = new Foo(); // Allocate memory for the object and initialize
...a... // Use the object (it is strongly reachable)
a = null; // A becomes unreachable (out of scope, nulled, etc)
// Eventually a collection occurs, and a's resources
// are torn down and the memory is freed
Dokud nebude možné objekt uvolnit, provedou se v obou světech stejné kroky. V nativním kódu je potřeba pamatovat na to, abyste objekt uvolnili, až ho přestanete používat. Jakmile ve spravovaném kódu přestane být objekt dostupný, může gc ho shromáždit. Samozřejmě, pokud váš prostředek vyžaduje zvláštní pozornost, aby byl uvolněn (například zavření soketu), může uvolňování paměti potřebovat pomoc se správným zavřením. Kód, který jste napsali dříve, abyste prostředek před uvolněním vyčistili, stále platí, a to ve formě metod Dispose() a Finalize(). O rozdílech mezi těmito dvěma si promluvím později.
Pokud necháte ukazatel na prostředek kolem, GC nemůže zjistit, jestli ho chcete v budoucnu použít. To znamená, že všechna pravidla, která jste použili v nativním kódu k explicitnímu uvolnění objektů, stále platí, ale ve většině případů bude uvolňování paměti vše zpracovávat za vás. Místo toho, abyste se staraly o správu paměti na sto procent času, se o ni budete muset starat jenom v pěti procentech času.
Systém uvolňování paměti CLR je generační kolektor se značkami a kompakty. Řídí se několika principy, které jí umožňují dosáhnout vynikajícího výkonu. Za prvé, existuje představa, že objekty, které jsou krátkodobé, bývají menší a jsou často přístupné. Katalog rozdělí graf přidělení do několika dílčích grafů označovaných jako generace, které mu umožňují strávit shromažďováním co nejkratšího času*.* Gen 0 obsahuje mladé, často používané objekty. To je také obvykle nejmenší a trvá asi 10 milisekund, než se nashromáždí. Vzhledem k tomu, že uvolňování paměti může během této kolekce ignorovat ostatní generace, poskytuje mnohem vyšší výkon. Objekty G1 a G2 jsou určené pro větší a starší objekty a shromažďují se méně často. Když dojde ke kolekci G1, shromáždí se také G0. Kolekce G2 je úplná kolekce a je to jediný čas, kdy uvolňování paměti prochází celým grafem. Také inteligentně využívá mezipaměti procesoru, které mohou vyladit paměťový subsystém pro konkrétní procesor, na kterém běží. Jedná se o optimalizaci, která není snadno dostupná v nativním přidělování a může vaší aplikaci pomoct zlepšit výkon.
Kdy ke kolekci dojde?
Když se provede přidělení času, GC zkontroluje, jestli je potřeba kolekce. Prohlédne si velikost kolekce, zbývající velikost paměti a velikosti jednotlivých generací a pak použije heuristické rozhodnutí. Dokud nedojde ke kolekci, je rychlost přidělování objektů obvykle stejně rychlá (nebo rychlejší) jako jazyk C nebo C++.
Co se stane, když dojde ke kolekci?
Pojďme si projít kroky, které systém uvolňování paměti provede během shromažďování. Uvolňování paměti udržuje seznam kořenových certifikátů, které ukazují na haldu uvolňování paměti. Pokud je objekt aktivní, existuje kořen pro jeho umístění v haldě. Objekty v haldě mohou také odkazovat na sebe navzájem. Tento graf ukazatelů je to, co musí uvolňování paměti prohledávat, aby uvolnilo místo. Pořadí událostí je následující:
Spravovaná halda uchovává veškerý prostor přidělení v souvislých blokech, a pokud je tento blok menší než požadovaná částka, je volána uvolňování paměti.
Uvolňování paměti sleduje každý kořen a všechny následující ukazatele a udržuje seznam objektů, které nejsou dostupné.
Každý objekt, který není dostupný z žádného kořenového adresáře, se považuje za shromažďovatelný a je označen ke shromažďování.
Obrázek 1: Před shromažďováním: Mějte na paměti, že ne všechny bloky jsou dostupné z kořenů.
Odebráním objektů z grafu dostupnosti se většina objektů dá shromažďovat. Některé prostředky je ale potřeba zpracovat speciálně. Při definování objektu máte možnost napsat metodu Dispose() nebo Metodu Finalize() (nebo obojí). Budu mluvit o rozdílech mezi těmito dvěma, a kdy je použít později.
Posledním krokem v kolekci je fáze komprimace. Všechny objekty, které se používají, se přesunou do souvislého bloku a všechny ukazatele a kořeny se aktualizují.
Komprimací živých objektů a aktualizací počáteční adresy volného místa GC udržuje, že veškeré volné místo je souvislé. Pokud je k dispozici dostatek místa pro přidělení objektu, GC vrátí řízení programu. Pokud ne, vyvolá se .
OutOfMemoryException
Obrázek 2. Po shromáždění: Dostupné bloky byly zkomprimovány. Víc volného místa!
Další technické informace o správě paměti naleznete v Kapitole 3 programovacích aplikací pro Microsoft Windows Jeffrey Richter (Microsoft Press, 1999).
Vyčištění objektu
Některé objekty vyžadují před vrácením svých prostředků speciální zpracování. Mezi příklady takových prostředků patří soubory, síťové sokety nebo databázová připojení. Pouhé uvolnění paměti na haldě nebude stačit, protože chcete, aby se tyto prostředky řádně uzavřely. Chcete-li provést vyčištění objektu, můžete napsat metodu Dispose(), Metodu Finalize() nebo obojí.
Metoda Finalize():
- Je volána uvolňováním paměti.
- Není zaručeno, že bude volán v libovolném pořadí nebo v předvídatelné době.
- Po zavolání uvolní paměť po dalším uvolňování paměti.
- Udržuje všechny podřízené objekty aktivní až do dalšího uvolňování paměti.
Metoda Dispose():
- Je volána programátorem
- Je seřazeno a naplánováno programátorem.
- Po dokončení metody vrátí prostředky.
Spravované objekty, které obsahují pouze spravované prostředky, tyto metody nevyžadují. Váš program bude pravděpodobně používat jen několik složitých prostředků a je pravděpodobné, že víte, jaké jsou a kdy je potřebujete. Pokud znáte obě tyto věci, není důvod spoléhat se na finalizační metody, protože čištění můžete provést ručně. Existuje několik důvodů, proč to chcete udělat, a všechny mají co dělat s frontou finalizační metody.
Pokud je objekt, který má finalizační metodu, označený jako shromažďovatelný, umístí se i všechny objekty, na které odkazuje, do speciální fronty. Tuto frontu prochází samostatné vlákno, které volá metodu Finalize() každé položky ve frontě. Programátor nemá žádnou kontrolu nad tímto vláknem nebo pořadím položek umístěných ve frontě. Uvolňování paměti může vrátit řízení programu, aniž by bylo dokončeno žádné objekty ve frontě. Tyto objekty můžou zůstat v paměti zastrčené ve frontě po dlouhou dobu. Volání k dokončení se provádějí automaticky a samotné volání nemá žádný přímý dopad na výkon. Ne deterministický model pro finalizaci však může mít určitě i jiné nepřímé důsledky:
- Ve scénáři, kdy máte prostředky, které je potřeba vydat v určitém čase, ztratíte kontrolu nad finalizačními prostředky. Řekněme, že máte otevřený soubor, který je potřeba z bezpečnostních důvodů zavřít. I když nastavíte objekt na hodnotu null a okamžitě vynutíte uvolňování paměti, zůstane soubor otevřený, dokud není volána metoda Finalize() a nemáte ponětí, kdy by k tomu mohlo dojít.
- N objektů, které vyžadují likvidaci v určitém pořadí, nemusí být správně zpracovány.
- Obrovský objekt a jeho děti mohou zabírat příliš mnoho paměti, vyžadovat další kolekce a poškodit výkon. Tyto objekty se nemusí shromažďovat po dlouhou dobu.
- Malý objekt, který má být dokončen, může mít ukazatele na velké prostředky, které by bylo možné kdykoli uvolnit. Tyto objekty nebudou uvolněny, dokud se o objekt, který má být dokončen, postaráno, což způsobí zbytečné zatížení paměti a vynucení častých kolekcí.
Diagram stavu na obrázku 3 znázorňuje různé cesty, které může objekt probírat z hlediska finalizace nebo vyřazení.
Obrázek 3: Odstranění a finalizace cest, které objekt může přijmout
Jak vidíte, dokončení přidá několik kroků k životnosti objektu. Pokud objekt odstraníte sami, můžete ho shromáždit a paměť vám vrátit v dalším uvolňování paměti. Když je potřeba dokončit, musíte počkat, než se zavolá skutečná metoda. Vzhledem k tomu, že nemáte žádné záruky, kdy k tomu dojde, můžete mít spoustu paměti svázané a být na milost s finalizační frontou. To může být velmi problematické, pokud je objekt připojený k celému stromu objektů a všechny se nacházejí v paměti, dokud nedojde k dokončení.
Výběr toho, který systém uvolňování paměti se má použít
ClR má dvě různé gC: pracovní stanice (mscorwks.dll) a server (mscorsvr.dll). Při spuštění v režimu pracovní stanice je latence důležitější než prostor nebo efektivita. Server s více procesory a klienty připojenými přes síť si může dovolit určitou latenci, ale propustnost je teď nejvyšší prioritou. Místo toho, aby se oba tyto scénáře vložily do jednoho schématu uvolňování paměti, microsoft zahrnul dva sběrače paměti, které jsou přizpůsobené každé situaci.
Uvolňování paměti serveru:
- Škálovatelné, paralelní multiprocesorové (MP)
- Jedno vlákno uvolňování paměti na procesor
- Program se během označování pozastavil.
Uvolňování paměti pracovní stanice:
- Minimalizuje pozastavení souběžným spouštěním během úplných kolekcí.
Uvolňování paměti serveru je navržené pro maximální propustnost a škáluje se s velmi vysokým výkonem. Fragmentace paměti na serverech je mnohem vážnější problém než na pracovních stanicích, takže uvolňování paměti je atraktivní. Ve scénáři s jednoprocesorem fungují oba kolektory stejným způsobem: režim pracovní stanice, bez souběžného shromažďování. Na počítači MP používá uvolňování paměti pracovní stanice k souběžnému spuštění kolekce druhý procesor, čímž minimalizuje zpoždění a snižuje propustnost. Uvolňování paměti serveru používá více podprocesů a kolekcí k maximalizaci propustnosti a lepšímu škálování.
Můžete zvolit, který uvolňování paměti se má použít při hostování doby běhu. Při načítání doby spuštění do procesu určíte, jaký kolektor se má použít. Načtení rozhraní API je popsáno v příručce pro vývojáře rozhraní .NET Framework. Příklad jednoduchého programu, který hostuje dobu běhu a vybere uvolňování paměti serveru, najdete v příloze.
Mýt: Uvolňování paměti je vždy pomalejší než ruční
Ve skutečnosti, dokud není volána kolekce, je uvolňování paměti mnohem rychlejší než ručně v jazyce C. To překvapí spoustu lidí, takže to stojí za vysvětlení. Nejprve si všimněte, že hledání volného místa probíhá v konstantním čase. Vzhledem k tomu, že veškeré volné místo je souvislé, GC jednoduše následuje ukazatel a zkontroluje, jestli je k dispozici dostatek místa. Volání malloc()
v jazyce C obvykle
vede k vyhledání propojeného seznamu volných bloků. To může být časově náročné, zejména pokud je vaše halda špatně fragmentovaná. Aby toho nebylo málo, několik implementací doby spuštění jazyka C během tohoto postupu haldu uzamkne. Jakmile je paměť přidělena nebo využita, musí se seznam aktualizovat. V prostředí uvolňování paměti je přidělení bezplatné a paměť se uvolní během shromažďování. Pokročilejší programátoři si vyrezervují velké bloky paměti a zajistí přidělení v rámci tohoto bloku sami. Problémem tohoto přístupu je, že fragmentace paměti se stává obrovským problémem pro programátory a nutí je přidat do svých aplikací spoustu logiky zpracování paměti. Systém uvolňování paměti nakonec nepřidá velkou režii. Přidělování je stejně rychlé nebo rychlejší a komprimace se zpracovává automaticky, což programátorům umožňuje soustředit se na své aplikace.
V budoucnu by systém uvolňování paměti mohl provádět další optimalizace, které ho ještě urychlí. Identifikace aktivních bodů a lepší využití mezipaměti je možné a může výrazně lišit rychlost. Inteligentnější uvolňování paměti by mohlo stránky sbalit efektivněji a minimalizovat tak počet načítání stránek, ke kterým dochází během provádění. Všechny tyto možnosti by mohly prostředí s uvolňováním paměti urychlit než ruční práce.
Někteří lidé se mohou divit, proč uvolňování paměti není k dispozici v jiných prostředích, jako je C nebo C++. Odpovědí jsou typy. Tyto jazyky umožňují přetypování ukazatelů na libovolný typ, takže je velmi obtížné zjistit, na co ukazatel odkazuje. Ve spravovaném prostředí, jako je CLR, můžeme zaručit dostatečné množství ukazatelů, aby bylo možné uvolňování paměti. Spravovaný svět je také jediným místem, kde můžeme bezpečně zastavit spouštění vláken za účelem provedení uvolňování paměti: v jazyce C++ jsou tyto operace buď nebezpečné, nebo jsou velmi omezené.
Ladění rychlosti
Největší starostí programu ve spravovaném světě je uchovávání paměti. Některé z problémů, na které narazíte v nespravovaných prostředích, nejsou ve spravovaném světě problémem: nevrácená paměť a visící ukazatele nejsou v tomto prostředí příliš velkým problémem. Místo toho musí programátoři dávat pozor na to, aby nechali připojené prostředky, když je už nepotřebují.
Nejdůležitější heuristiku výkonu se také nejsnadněji naučí programátoři, kteří jsou zvyklí psát nativní kód: sledujte přidělení, která se mají provést, a až skončíte, uvolníte je. GC nemůže zjistit, že nebudete používat řetězec o délce 20 kB, který jste vytvořili, pokud je součástí objektu, který je udržován kolem. Předpokládejme, že máte tento objekt někde schovaný ve vektoru a tento řetězec už nikdy nechcete použít. Nastavení pole na hodnotu null umožní uvolňování paměti shromáždit těchto 20 kB později, i když objekt stále potřebujete pro jiné účely. Pokud už objekt nepotřebujete, ujistěte se, že na něj neuchováte odkazy. (Stejně jako v nativním kódu.) U menších objektů to není problém. Žádný programátor, který je obeznámen se správou paměti v nativním kódu, zde nebude mít žádný problém: platí stejná pravidla selského rozumu. Jen kvůli nim nemusíš být tak paranoidní.
Druhý důležitý problém s výkonem se zabývá čištěním objektů. Jak už jsem zmínil dříve, finalizace má výrazný dopad na výkon. Nejběžnějším příkladem spravované obslužné rutiny pro nespravovaný prostředek je potřeba implementovat nějaký druh metody čištění a tady se stává problém s výkonem. Pokud jste závislí na finalizaci, otevřete se problémům s výkonem, které jsem uvedl dříve. Ještě je potřeba mít na paměti, že uvolňování paměti v nativním světě z velké části neví, takže možná používáte tunu nespravovaných prostředků jen tak, že ve spravované haldě udržujete ukazatel. Jeden ukazatel nezabere moc paměti, takže může chvíli trvat, než bude potřeba kolekce. Pokud chcete tyto problémy s výkonem obejít a zároveň je stále přehrávat bezpečně, pokud jde o uchovávání paměti, měli byste vybrat vzor návrhu pro práci se všemi objekty, které vyžadují speciální vyčištění.
Programátor má při čištění objektů čtyři možnosti:
Implementace obojího
Toto je doporučený návrh pro čištění objektů. Jedná se o objekt s určitou kombinací nespravovaných a spravovaných prostředků. Příkladem může být System.Windows.Forms.Control. Má nespravovaný prostředek (HWND) a potenciálně spravované prostředky (DataConnection atd.). Pokud si nejste jistí, kdy používáte nespravované prostředky, můžete otevřít manifest pro váš program v
ILDASM``
a vyhledat odkazy na nativní knihovny. Další alternativou je zjistitvadump.exe
, jaké prostředky se načítají spolu s programem. Obě tyto možnosti vám můžou poskytnout přehled o tom, jaký druh nativních prostředků používáte.Následující vzor poskytuje uživatelům jeden doporučený způsob, místo aby přepisovali logiku čištění (přepište Dispose(bool)). To poskytuje maximální flexibilitu, stejně jako univerzální univerzální řešení pro případ, že se Dispose() nikdy nevolá. Kombinace maximální rychlosti a flexibility, stejně jako přístup bezpečnostní sítě, činí z tohoto návrhu nejlepší použití.
Příklad:
public class MyClass : IDisposable { public void Dispose() { Dispose(true); GC.SuppressFinalizer(this); } protected virtual void Dispose(bool disposing) { if (disposing) { ... } ... } ~MyClass() { Dispose(false); } }
Pouze implementace Dispose()
To je, když má objekt pouze spravované prostředky a chcete se ujistit, že jeho vyčištění je deterministické. Příkladem takového objektu je System.Web.UI.Control.
Příklad:
public class MyClass : IDisposable { public virtual void Dispose() { ... }
Implementovat pouze finalize()
To je zapotřebí v extrémně vzácných situacích, a důrazně doporučuji proti tomu. Implikací pouze objektu Finalize() je, že programátor nemá ponětí, kdy se objekt bude shromažďovat, a přesto používá dostatečně složitý prostředek, aby vyžadoval speciální vyčištění. Tato situace by nikdy neměla nastat v dobře navrženém projektu, a pokud se v něm nacházíte, měli byste se vrátit a zjistit, co se nepovedlo.
Příklad:
public class MyClass { ... ~MyClass() { ... }
Implementace ani jednoho z nich
To platí pro spravovaný objekt, který odkazuje pouze na jiné spravované objekty, které nejsou k dispozici ani se nemají dokončit.
Doporučení
Doporučení pro práci se správou paměti by měla být známá: uvolnit objekty, jakmile s nimi skončíte, a dávat pozor na ponechání ukazatelů na objekty. Pokud jde o vyčištění objektů, implementujte metodu Finalize() a Dispose()
pro objekty s nespravovanými prostředky. Tím zabráníte neočekávanému chování později a vynutíte správné programovací postupy.
Nevýhodou je, že nutíte lidi, aby museli volat Dispose(). Tady nedochází ke ztrátě výkonu, ale pro některé lidi může být frustrující přemýšlet o likvidaci svých objektů. Nicméně, myslím, že stojí za zhoršení použít model, který dává smysl. Kromě toho to nutí lidi být pozornější k objektům, které přidělují, protože nemohou slepě důvěřovat GC, že se o ně vždy postará. Pro programátory, kteří pocházejí z C nebo C++ pozadí, bude vynucení volání Dispose() pravděpodobně přínosné, protože je to typ věci, kterou znají lépe.
Dispose() by měla být podporována u objektů, které drží nespravované prostředky kdekoli ve stromu objektů pod ním; Finalize() však musí být umístěna pouze na objekty, které konkrétně drží na těchto prostředcích, jako je popisovač operačního systému nebo nespravované přidělení paměti. Pro implementaci finalize() a podpory Dispose(), která by byla volána objektem Dispose(), ,
doporučujeme vytvořit malé spravované objekty jako "obálky". Vzhledem k tomu, že nadřazené objekty nemají finalizační prvek, celý strom objektů nepřežije kolekci bez ohledu na to, zda byl nebo nebyl volána funkce Dispose().
Dobrým pravidlem pro finalizátory je použít je pouze u nejprimitivnějšího objektu, který vyžaduje finalizaci. Předpokládejme, že mám velký spravovaný prostředek, který zahrnuje připojení k databázi: Umožnil bych dokončení samotného připojení, ale zbytek objektu by byl jednorázový. Tímto způsobem můžu volat Dispose() a okamžitě uvolnit spravované části objektu, aniž bych musel čekat na dokončení připojení. Nezapomeňte: Finalize() používejte jenom tam, kde je to nutné a když je to nutné.
Poznámka Programátoři jazyka C a C++: Sémantika destruktoru v jazyce C# vytvoří finalizační metodu, nikoli metodu likvidace.
Fond vláken
Základy
Fond vláken CLR je v mnoha ohledech podobný fondu vláken NT a nevyžaduje téměř žádné nové pochopení ze strany programátora. Má čekací vlákno, které dokáže zpracovat bloky pro jiná vlákna a upozornit je, když se potřebují vrátit, a uvolnit je tak, aby mohli dělat jinou práci. Může vytvořit nová vlákna a blokovat ostatní pro optimalizaci využití procesoru za běhu, a zaručit tak, že se provede největší množství užitečné práce. Také recykluje vlákna, jakmile jsou hotovi, a znovu je spustí bez režie zabíjení a vytváření nových. Jedná se o výrazné zvýšení výkonu oproti ručnímu zpracování vláken, ale nejedná se o univerzální řešení. Při ladění aplikace s vlákny je důležité vědět, kdy použít fond vláken.
Co znáte z fondu vláken NT:
- Fond vláken bude zpracovávat vytváření a čištění vláken.
- Poskytuje port pro dokončení pro vstupně-výstupní vlákna (pouze platformy NT).
- Zpětné volání může být vázáno na soubory nebo jiné systémové prostředky.
- K dispozici jsou rozhraní API časovače a čekání.
- Fond vláken určuje, kolik vláken má být aktivních, pomocí heuristiky, jako je zpoždění od poslední injektáže, počet aktuálních vláken a velikost fronty.
- Vlákna se chytnou ze sdílené fronty.
Co se liší v .NET:
- Ví o blokování vláken ve spravovaném kódu (např. kvůli uvolňování paměti nebo spravovanému čekání) a může odpovídajícím způsobem upravit logiku injektáže vlákna.
- Pro jednotlivá vlákna není zaručena služba.
Kdy zpracovávat vlákna sami
Efektivní používání fondu vláken úzce souvisí s tím, že víte, co z vašich vláken potřebujete. Pokud potřebujete záruku služby, budete ji muset spravovat sami. Ve většině případů vám použití fondu zajistí optimální výkon. Pokud máte pevná omezení a potřebujete pevnou kontrolu nad svými vlákny, bude pravděpodobně dávat větší smysl používat nativní vlákna, takže buďte opatrní při manipulaci se spravovanými vlákny sami. Pokud se rozhodnete psát spravovaný kód a zpracovávat vlákna sami, ujistěte se, že nevytváříte vlákna pro jednotlivá připojení: to bude jen poškodit výkon. Obecně platí, že byste se měli rozhodnout zpracovávat vlákna sami ve spravovaném světě ve velmi specifických scénářích, kde je velký a časově náročný úkol, který se provádí zřídka. Jedním z příkladů může být vyplnění velké mezipaměti na pozadí nebo zápis velkého souboru na disk.
Ladění rychlosti
Fond vláken nastaví limit počtu vláken, která by měla být aktivní, a pokud je jich mnoho blokovaných, fond bude hladovět. V ideálním případě byste měli fond vláken použít pro krátkodobá neblokující vlákna. V serverových aplikacích chcete rychle a efektivně odpovědět na každý požadavek. Pokud spustíte nové vlákno pro každý požadavek, budete řešit velké režie. Řešením je recyklovat vlákna a po dokončení vyčistit a vrátit stav každého vlákna. Jedná se o scénáře, kdy fond vláken představuje hlavní výhru výkonu a návrhu a kde byste měli tuto technologii dobře využít. Fond vláken zpracovává vyčištění stavu za vás a zajišťuje, aby se v daném okamžiku používal optimální počet vláken. V jiných situacích může být vhodnější zpracovávat vlákna sami.
I když CLR může pomocí zabezpečení typu poskytnout záruky týkající se procesů, aby zajistil, že AppDomains mohou sdílet stejný proces, žádná taková záruka neexistuje u vláken. Programátor je zodpovědný za psaní dobře chovaných vláken a všechny vaše znalosti z nativního kódu stále platí.
Níže uvádíme příklad jednoduché aplikace, která využívá fond vláken. Vytvoří spoustu pracovních vláken a pak je nutí provést jednoduchou úlohu před jejich zavřením. Provedl(a) jsem kontrolu chyb, ale je to stejný kód, který najdete ve složce Framework SDK v části Samples\Threading\Threadpool. V tomto příkladu máme kód, který vytvoří jednoduchou pracovní položku a používá fond vláken k tomu, aby tyto položky zpracovávalo více vláken, aniž by je programátor musel spravovat. Další informace najdete v souboru ReadMe.html.
using System;
using System.Threading;
public class SomeState{
public int Cookie;
public SomeState(int iCookie){
Cookie = iCookie;
}
};
public class Alpha{
public int [] HashCount;
public ManualResetEvent eventX;
public static int iCount = 0;
public static int iMaxCount = 0;
public Alpha(int MaxCount) {
HashCount = new int[30];
iMaxCount = MaxCount;
}
// The method that will be called when the Work Item is serviced
// on the Thread Pool
public void Beta(Object state){
Console.WriteLine(" {0} {1} :",
Thread.CurrentThread.GetHashCode(), ((SomeState)state).Cookie);
Interlocked.Increment(ref HashCount[Thread.CurrentThread.GetHashCode()]);
// Do some busy work
int iX = 10000;
while (iX > 0){ iX--;}
if (Interlocked.Increment(ref iCount) == iMaxCount) {
Console.WriteLine("Setting EventX ");
eventX.Set();
}
}
};
public class SimplePool{
public static int Main(String[] args) {
Console.WriteLine("Thread Simple Thread Pool Sample");
int MaxCount = 1000;
ManualResetEvent eventX = new ManualResetEvent(false);
Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount);
Alpha oAlpha = new Alpha(MaxCount);
oAlpha.eventX = eventX;
Console.WriteLine("Queue to Thread Pool 0");
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),new SomeState(0));
for (int iItem=1;iItem < MaxCount;iItem++){
Console.WriteLine("Queue to Thread Pool {0}", iItem);
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),
new SomeState(iItem));
}
Console.WriteLine("Waiting for Thread Pool to drain");
eventX.WaitOne(Timeout.Infinite,true);
Console.WriteLine("Thread Pool has been drained (Event fired)");
Console.WriteLine("Load across threads");
for(int iIndex=0;iIndex<oAlpha.HashCount.Length;iIndex++)
Console.WriteLine("{0} {1}", iIndex, oAlpha.HashCount[iIndex]);
}
return 0;
}
}
The JIT
Základy
Stejně jako u každého virtuálního počítače potřebuje CLR způsob, jak zkompilovat zprostředkující jazyk do nativního kódu. Při kompilaci programu pro spuštění v CLR kompilátor přenese váš zdroj z jazyka vysoké úrovně na kombinaci MSIL (Microsoft Intermediate Language) a metadat. Tyto soubory se sloučí do souboru PE, který se pak dá spustit na libovolném počítači s podporou CLR. Když spustíte tento spustitelný soubor, jit začne kompilovat il do nativního kódu a spouštět tento kód na skutečném počítači. To se provádí na základě jednotlivých metod, takže zpoždění pro JITing je pouze tak dlouho, jak je potřeba pro kód, který chcete spustit.
JIT je poměrně rychlý a generuje velmi dobrý kód. Některé z optimalizací, které provádí (a některá vysvětlení každé z nich), jsou popsány níže. Mějte na paměti, že většina těchto optimalizací má stanovené limity, aby se zajistilo, že JIT nebude trávit příliš mnoho času.
Konstantní skládání – výpočet konstantních hodnot v době kompilace
Před Po x = 5 + 7
x = 12
Šíření konstant a kopírování – nahraďte zpět volnými proměnnými dříve.
Před Po x = a
x = a
y = x
y = a
z = 3 + y
z = 3 + a
Vkládání metod – nahraďte args hodnotami předanými při volání a eliminujte volání. Pak je možné provést mnoho dalších optimalizací, které vyříznou nedosažený kód. Z důvodů rychlosti má aktuální JIT několik hranic, co může být vloženo. Například jsou vloženy pouze malé metody (velikost IL menší než 32) a analýza řízení toků je poměrně primitivní.
Před Po ...
x=foo(4, true);
...
}
foo(int a, bool b){
if(b){
return a + 5;
} else {
return 2a + bar();
}
...
x = 9
...
}
foo(int a, bool b){
if(b){
return a + 5;
} else {
return 2a + bar();
}
Zvedání kódu a dominátory – Odebere kód z vnitřních smyček, pokud je duplikovaný mimo. Níže uvedený příklad "před" je ve skutečnosti to, co se vygeneruje na úrovni IL, protože je třeba zkontrolovat všechny indexy polí.
Před Po for(i=0; i< a.length;i++){
if(i < a.length()){
a[i] = null
} else {
raise IndexOutOfBounds;
}
}
for(int i=0; i<a.length; i++){
a[i] = null;
}
Zrušení registrace smyčky – režijní náklady na zvýšení čítačů a provádění testu je možné odebrat a kód smyčky se může opakovat. U extrémně těsných smyček to vede k výhře výkonu.
Před Po for(i=0; i< 3; i++){
print("flaming monkeys!");
}
print("flaming monkeys!");
print("flaming monkeys!");
print("flaming monkeys!");
Odstranění podvýrazu – pokud proměnná za provozu stále obsahuje informace, které se přepočítávají, použijte ji místo toho.
Před Po x = 4 + y
z = 4 + y
x = 4 + y
z = x
Enregistration – zde není vhodné uvést příklad kódu, takže bude muset stačit vysvětlení. Tato optimalizace může trávit čas sledováním toho, jak se ve funkci používají místní hodnoty a temps, a pokusit se zpracovat přiřazení registru co nejefektivněji. To může být extrémně nákladná optimalizace a aktuální CLR JIT bere v úvahu pouze maximálně 64 místních proměnných pro registraci. Proměnné, které nejsou brány v úvahu, jsou umístěny v rámci zásobníku. Toto je klasický příklad omezení JITingu: i když je to v 99 % času v pořádku, velmi neobvyklé funkce, které mají více než 100 místních hodnot, budou optimalizovány lépe pomocí tradiční a časově náročné předkompilační kompilace.
Různé – Provádí se další jednoduché optimalizace, ale dobrým vzorek je výše uvedený seznam. JIT také předává pro neaktivní kód a další optimalizace kukátkem.
Kdy dojde k jitedu kódu?
Tady je cesta, kterou váš kód prochází při spuštění:
- Program se načte a inicializuje se tabulka funkcí s ukazateli odkazujícími na IL.
- Metoda Main se jituje do nativního kódu, který se pak spustí. Volání funkcí se kompilují do nepřímých volání funkcí prostřednictvím tabulky.
- Při zavolání jiné metody se doba běhu podívá na tabulku a zjistí, jestli odkazuje na kód jited.
- Pokud ano (možná byla volána z jiného webu volání nebo byla předkompilována), tok řízení pokračuje.
- Pokud ne, metoda se změní na JIT a tabulka se aktualizuje.
- Při jejich zavolání se do nativního kódu kompiluje stále více metod a více položek v tabulce odkazuje na rostoucí fond instrukcí x86.
- Při běhu programu se JIT nazývá méně a méně často, dokud není všechno zkompilováno.
- Metoda není jiTed, dokud není volána, a pak se nikdy jituje znovu během provádění programu. Platíte jenom za to, co používáte.
Mýt: Programy s jiTed spouštějí pomaleji než předkompilované programy
To je zřídkakdy případ. Režie spojená s JITing několika metodami je menší v porovnání s časem stráveným čtením na několika stránkách z disku a metody jsou jitovány pouze podle potřeby. Čas strávený jit je tak malý, že je téměř nikdy znatelný, a jakmile je metoda jitována, už nikdy neúčtují náklady na tuto metodu. Více se o tom dozvím v části Předkompilování kódu.
Jak je uvedeno výše, jit verze1 (v1) dělá většinu optimalizací, které kompilátor dělá, a v další verzi (vNext) bude pouze rychlejší, jakmile se přidají pokročilejší optimalizace. Důležitější je, že JIT může provádět některé optimalizace, které běžný kompilátor nemůže, například optimalizace specifické pro procesor a ladění mezipaměti.
Optimalizace JIT-Only
Vzhledem k tomu, že se JIT aktivuje za běhu, existuje kolem velké množství informací, o kterých kompilátor neví. Díky tomu může provádět několik optimalizací, které jsou k dispozici pouze za běhu:
- Optimalizace specifické pro procesor– za běhu jit ví, jestli může využít instrukce SSE nebo 3DNow. Spustitelný soubor bude zkompilován speciálně pro P4, Athlon nebo jakékoli budoucí rodiny procesorů. Nasazení provedete jednou a stejný kód se vylepší společně s JIT a počítačem uživatele.
- Optimalizace úrovní vzdáleného rozdělení, protože funkce a umístění objektů jsou k dispozici za běhu.
- JIT může provádět optimalizace napříč sestaveními a poskytuje mnoho výhod, které získáte při kompilaci programu se statickými knihovnami, ale zachováte flexibilitu a malé nároky na používání dynamických.
- Agresivně vložené funkce , které se volají častěji, protože si je vědoma toku řízení během doby běhu. Optimalizace mohou poskytnout výrazné zvýšení rychlosti a je zde velký prostor pro další vylepšení vNext.
Tato vylepšení doby běhu přicházejí na úkor malých jednorázových nákladů na spuštění a mohou více než vyrovnat čas strávený v JIT.
Předkompilování kódu (pomocí ngen.exe)
Pro dodavatele aplikace je možnost předkompilovat kód během instalace atraktivní možností. Microsoft poskytuje tuto možnost ve formátu ngen.exe
, který vám umožní spustit normální kompilátor JIT v celém programu jednou a uložit výsledek. Vzhledem k tomu, že optimalizace pouze za běhu nelze provádět během předkompilace, není vygenerovaný kód obvykle tak dobrý jako kód generovaný normálním JIT. Bez nutnosti běhu metod JIT jsou však náklady na spuštění mnohem nižší a některé programy se spustí znatelně rychleji. V budoucnu může ngen.exe dělat víc než jednoduše spustit stejnou dobu běhu JIT: agresivnější optimalizace s vyššími hranicemi, než je doba běhu, vystavení vývojářům v optimalizaci pořadí načítání (optimalizace způsobu balení kódu do stránek virtuálních počítačů) a složitější a časově náročné optimalizace, které můžou využít čas během předkompilace.
Snížení doby spuštění pomáhá ve dvou případech a u všeho ostatního nekonkuruje optimalizacím pouze za běhu, které může provádět běžné JITing. V první situaci voláte v rané fázi programu obrovské množství metod. Budete muset předem provést jiT s mnoha metodami, což způsobí nepřijatelnou dobu načítání. To nebude případ většiny lidí, ale pre-JITing může mít smysl, pokud se vás týká. Předkompilování má smysl také v případě velkých sdílených knihoven, protože platíte náklady na jejich načítání mnohem častěji. Microsoft předkompiliuje rozhraní pro CLR, protože většina aplikací je bude používat.
Je snadné používat ngen.exe
zjistit, zda je předkompilování odpovědí pro vás, proto doporučuji vyzkoušet. Ve většině případů je ale ve skutečnosti lepší použít normální JIT a využít optimalizace za běhu. Mají obrovskou návratnost a ve většině situací více než vykompenzují jednorázové náklady na spuštění.
Ladění rychlosti
Pro programátora jsou opravdu jen dvě věci, které stojí za zmínku. Zaprvé, že JIT je velmi inteligentní. Nepokoušejte se přemýšlejte nad kompilátorem. Kódujte obvyklým způsobem. Předpokládejme například, že máte následující kód:
...
|
...
|
Někteří programátoři se domnívají, že mohou dosáhnout zrychlení přesunutím výpočtu délky a jeho uložením do tempa, jako v příkladu vpravo.
Pravdou je, že optimalizace, jako je tato, nebyly užitečné už téměř 10 let: moderní kompilátory jsou více než schopné provést tuto optimalizaci za vás. Ve skutečnosti, někdy takové věci mohou skutečně poškodit výkon. Ve výše uvedeném příkladu by kompilátor pravděpodobně zkontroloval, že délka myArray je konstantní, a vložil konstantu do porovnání smyčky for . Kód napravo ale může kompilátor oklamat, aby si myslel, že tato hodnota musí být uložená v registru, protože l
je aktivní v celé smyčce. Sečteno a podtrženo: napište kód, který je nejčitelný a který dává největší smysl. Nepomůže to, když se pokusíte přemýšlet nad kompilátorem a někdy to může ublížit.
Druhá věc, o které se mám bavit, jsou chvosty. V současné době vám kompilátory jazyka C# a Microsoft® Visual Basic® neposkytují možnost určit, že by se mělo použít koncové volání. Pokud tuto funkci opravdu potřebujete, jednou z možností je otevřít soubor PE v disassembleru a místo toho použít instrukci MSIL .tail. Nejedná se o elegantní řešení, ale koncová volání nejsou v jazyce C# a Visual Basic tak užitečná jako v jazycích, jako je Schéma nebo ML. Lidé psaní kompilátorů pro jazyky, které skutečně využívají koncová volání, byste měli používat tuto instrukci. Realitou pro většinu lidí je, že i ruční vyladění IL na použití tail-calls neposkytuje enormní výhodu rychlosti. Někdy se čas běhu z bezpečnostních důvodů změní zpět na běžná volání. Možná, že v budoucích verzích bude vynaloženo větší úsilí na podporu koncových volání, ale v tuto chvíli zvýšení výkonu nestačí, aby to bylo možné, a velmi málo programátorů bude chtít toho využít.
AppDomains
Základy
Meziprocesová komunikace je stále častější. Z důvodů stability a zabezpečení udržuje operační systém aplikace v oddělených adresních prostorech. Jednoduchým příkladem je způsob, jakým jsou všechny 16bitové aplikace spouštěné v systému NT: Pokud se spustí v samostatném procesu, nemůže jedna aplikace kolidovat s prováděním jiného procesu. Problémem jsou náklady na přepnutí kontextu a otevření propojení mezi procesy. Tato operace je velmi nákladná a hodně škodí výkonu. V serverových aplikacích, které často hostují několik webových aplikací, se jedná o zásadní zatěžování výkonu i škálovatelnosti.
CLR zavádí koncept AppDomain, který se podobá procesu v tom, že se jedná o samostatný prostor pro aplikaci. Domény aplikace ale nejsou omezeny na 1 na proces. Díky zabezpečení typu poskytovanému spravovaným kódem je možné ve stejném procesu spustit dvě zcela nesouvisející domény AppDomains. Zvýšení výkonu je obrovské v situacích, kdy obvykle trávíte hodně času provádění v režii meziprocesové komunikace: IPC mezi sestaveními je pětkrát rychlejší než mezi procesy v nt. Když tyto náklady výrazně snížíte, získáte při návrhu programu zvýšení rychlosti i novou možnost: nyní dává smysl používat samostatné procesy tam, kde dříve mohly být příliš drahé. Schopnost spouštět více programů ve stejném procesu se stejným zabezpečením jako předtím má obrovský dopad na škálovatelnost a zabezpečení.
V operačním systému není k dispozici podpora pro AppDomains. Domény aplikace jsou zpracovávány hostitelem CLR, například těmi, které jsou přítomné v ASP.NET, spustitelným souborem prostředí nebo Microsoft Internet Explorerem. Můžete si také napsat vlastní. Každý hostitel určuje výchozí doménu, která se načte při prvním spuštění aplikace a zavře se pouze při ukončení procesu. Když do procesu načítáte jiná sestavení, můžete určit, že se načtou do konkrétní domény AppDomain, a pro každé z nich nastavit jiné zásady zabezpečení. To je podrobněji popsáno v dokumentaci k sadě Microsoft .NET Framework SDK.
Ladění rychlosti
Pokud chcete appdomains používat efektivně, musíte se zamyslet nad tím, jaký druh aplikace píšete a jaký druh práce je potřeba udělat. Obecně platí, že AppDomains jsou nejúčinnější, když vaše aplikace splňuje některé z následujících vlastností:
- Často vytváří novou kopii sebe sama.
- Spolupracuje s jinými aplikacemi na zpracování informací (například databázové dotazy uvnitř webového serveru).
- Tráví spoustu času v IPC s programy, které pracují výhradně s vaší aplikací.
- Otevře a zavře ostatní programy.
Příklad situace, kdy jsou appDomains užitečné, je možné vidět ve složité ASP.NET aplikaci. Předpokládejme, že chcete vynutit izolaci mezi různými vRoots: v nativním prostoru potřebujete umístit každý vRoot do samostatného procesu. To je poměrně nákladné a přepínání kontextu mezi nimi představuje velkou režii. Ve spravovaném světě může být každý vRoot samostatnou doménou AppDomain. Tím se zachová požadovaná izolace a zároveň se výrazně sníží režijní náklady.
AppDomains je něco, co byste měli používat jenom v případě, že je vaše aplikace dostatečně složitá, aby vyžadovala úzkou spolupráci s jinými procesy nebo jinými instancemi. I když je komunikace iter-AppDomain mnohem rychlejší než komunikace mezi procesy, náklady na spuštění a zavření domény AppDomain můžou být ve skutečnosti dražší. Při použití z nesprávných důvodů můžou domény AppDomains poškodit výkon, proto se ujistěte, že je používáte ve správných situacích. Všimněte si, že do domény AppDomain je možné načíst pouze spravovaný kód, protože nespravovaný kód nemůže být zabezpečen.
Sestavení, která jsou sdílena mezi několika doménami AppDomains, musí být pro každou doménu jitována, aby se zachovala izolace mezi doménami. Výsledkem je vytvoření velkého množství duplicitního kódu a plýtvání paměti. Představte si případ aplikace, která odpovídá na požadavky pomocí nějaké služby XML. Pokud se některé požadavky musí udržovat oddělené od sebe, budete je muset směrovat do různých domén AppDomains. Problém je v tom, že každá doména AppDomain teď bude vyžadovat stejné knihovny XML a stejné sestavení se načte vícekrát.
Jedním ze způsobů, jak to obejít, je deklarovat sestavení jako doménově neutrální, což znamená, že nejsou povoleny žádné přímé odkazy a izolace se vynucuje prostřednictvím zprostředkování. To šetří čas, protože sestavení je jitováno pouze jednou. Šetří také paměť, protože se nic duplikuje. Kvůli požadovanému nepřímému snížení výkonu bohužel došlo k dosažení výkonu. Deklarování sestavení jako neutrální z domény má za následek výhru výkonu, pokud je problém s pamětí nebo když je příliš mnoho času plýtvá kód JITing. Podobné scénáře jsou běžné v případě velkého sestavení, které je sdíleno několika doménami.
Zabezpečení
Základy
Zabezpečení přístupu kódu je výkonná a velmi užitečná funkce. Nabízí uživatelům bezpečné spouštění částečně důvěryhodného kódu, chrání před škodlivým softwarem a několika druhy útoků a umožňuje řízený přístup k prostředkům založeným na identitách. V nativním kódu je velmi obtížné zajistit zabezpečení, protože je málo zabezpečení typů a programátor zpracovává paměť. V CLR ví doba běhu dostatek o spuštění kódu, aby se přidala silná podpora zabezpečení, funkce, která je pro většinu programátorů nová.
Zabezpečení ovlivňuje rychlost i velikost pracovní sady aplikace. A stejně jako u většiny oblastí programování může způsob, jakým vývojář používá zabezpečení, výrazně určit jeho dopad na výkon. Systém zabezpečení je navržený s ohledem na výkon a ve většině případů by měl fungovat dobře s minimálním nebo žádným nápadem, který vývojář aplikace zadal. Existuje však několik věcí, které můžete udělat, abyste z bezpečnostního systému vyždímat vůbec poslední kousek výkonu.
Ladění rychlosti
Provedení kontroly zabezpečení obvykle vyžaduje trasu zásobníku, která zajistí, že kód volající aktuální metodu má správná oprávnění. Doba běhu má několik optimalizací, které jí pomůžou vyhnout se procházení celého zásobníku, ale programátor může pomoct několika věcmi. Tím se dostáváme k pojmu imperativního a deklarativního zabezpečení: deklarativní zabezpečení zdobilo typ nebo jeho členy s různými oprávněními, zatímco imperativní zabezpečení vytváří objekt zabezpečení a provádí s ním operace.
- Deklarativní zabezpečení je nejrychlejší způsob, jak přejít k Assert, Deny a PermitOnly. Tyto operace obvykle vyžadují trasu zásobníku k vyhledání správného rámce volání, ale můžete tomu zabránit, pokud tyto modifikátory explicitně deklarujete. Požadavky jsou rychlejší, pokud se provádí imperativním způsobem.
- Při spolupráci s nespravovaným kódem můžete odebrat kontroly zabezpečení za běhu pomocí atributu SuppressUnmanagedCodeSecurity. Tím se kontrola přesune do času propojení, což je mnohem rychlejší. Jako upozornění se ujistěte, že kód nevystavuje žádné bezpečnostní díry jinému kódu, který by mohl zneužít odebranou kontrolu nebezpečného kódu.
- Kontroly identit jsou dražší než kontroly kódu. K provedení těchto kontrol v době propojení můžete použít LinkDemand.
Zabezpečení můžete optimalizovat dvěma způsoby:
- Provádějte kontroly v době propojení místo v době běhu.
- Proveďte kontroly zabezpečení deklarativní, nikoli imperativní.
První věc, na kterou byste se měli soustředit, je přesunout co nejvíce těchto kontrol, abyste propojili čas, jak je to možné. Mějte na paměti, že to může mít vliv na zabezpečení vaší aplikace, proto se ujistěte, že nepřesunujete kontroly do linkeru, které závisí na stavu běhu. Jakmile se co nejvíce přesunete do doby propojení, měli byste optimalizovat kontroly za běhu pomocí deklarativního nebo imperativního zabezpečení: zvolte, která je optimální pro konkrétní druh kontroly, kterou používáte.
Remoting
Základy
Technologie vzdálené komunikace v .NET rozšiřuje bohatý systém typů a funkce CLR v síti. Pomocí XML, SOAP a HTTP můžete volat procedury a předávat objekty vzdáleně, stejně jako kdyby byly hostované ve stejném počítači. Můžete si to představit jako verzi .NET DCOM nebo CORBA, protože poskytuje nadmnožinu jejich funkcí.
To je užitečné zejména v serverovém prostředí, když máte několik serverů, které hostují různé služby a všechny vzájemně komunikují, aby tyto služby hladce propojily. Vylepšili jsme také škálovatelnost, protože procesy se dají fyzicky rozdělit do více počítačů, aniž by došlo ke ztrátě funkčnosti.
Ladění rychlosti
Vzhledem k tomu, že při vzdálené komunikaci často dochází k penalizaci z hlediska latence sítě, platí pro CLR stejná pravidla, která platí vždy: snažte se minimalizovat množství odesílaného provozu a vyhněte se tomu, aby zbytek programu čekal na vzdálené volání, které se vrátí. Tady je několik dobrých pravidel, podle kterých byste se při použití vzdálené komunikace k maximalizaci výkonu chytili:
- Místo chatrných hovorů – Podívejte se, jestli můžete snížit počet hovorů, které musíte uskutečnit vzdáleně. Předpokládejme například, že jste některé vlastnosti vzdáleného objektu nastavili pomocí metod get() a set(). Ušetřilo by vám to čas jednoduše vzdáleně vytvořit objekt s těmito vlastnostmi nastavenými při vytvoření. Vzhledem k tomu, že se to dá provést jedním vzdáleným voláním, ušetříte tak čas strávený síťovým provozem. Někdy může být vhodné objekt přesunout do místního počítače, nastavit tam vlastnosti a pak ho zkopírovat zpět. V závislosti na šířce pásma a latenci bude někdy dávat jedno řešení větší smysl než to druhé.
- Vyrovnávání zatížení procesoru a zatížení sítě – někdy má smysl poslat něco, co se má udělat přes síť, a jindy je lepší udělat práci sami. Pokud plýtváte hodně času procházením sítě, váš výkon se zhorší. Pokud využíváte příliš mnoho procesoru, nebudete moct odpovídat na jiné požadavky. Nalezení dobré rovnováhy mezi těmito dvěma je nezbytné pro zajištění škálování vaší aplikace.
- Použití asynchronních volání : Pokud volání přes síť provádíte, ujistěte se, že je asynchronní, pokud opravdu nepotřebujete jinak. V opačném případě se vaše aplikace zastaví, dokud neobdrží odpověď, a to může být nepřijatelné v uživatelském rozhraní nebo na serveru s velkými objemy. Dobrý příklad, na který se můžete podívat, je k dispozici v sadě Framework SDK, která se dodává s .NET v části Samples\technologies\remoting\advanced\asyncdelegate.
- Optimální použití objektů – Můžete určit, že se pro každý požadavek vytvoří nový objekt (SingleCall) nebo že se stejný objekt použije pro všechny požadavky (Singleton). Mít jeden objekt pro všechny požadavky je určitě méně náročné na prostředky, ale budete muset být opatrní při synchronizaci a konfiguraci objektu od požadavku k žádosti.
- Využijte připojitelné kanály a formátovací moduly – Výkonnou funkcí vzdálené komunikace je možnost zapojit do aplikace libovolný kanál nebo formátovací modul. Pokud se například nepotřebujete dostat přes bránu firewall, není důvod používat kanál HTTP. Zapojením kanálu TCP získáte mnohem lepší výkon. Ujistěte se, že jste zvolili kanál nebo formátovací modul, který je pro vás nejvhodnější.
Typy hodnot
Základy
Flexibilita, kterou objekty poskytují, je za malou výkonovou cenu. Přidělení, přístup a aktualizace objektů spravovaných haldou trvá déle než objektů spravovaných zásobníkem. To je důvod, proč je například struktura v jazyce C++ mnohem efektivnější než objekt. Objekty samozřejmě dokážou věci, které struktury neumí, a jsou mnohem všestrannější.
Někdy ale nebudete potřebovat veškerou flexibilitu. Někdy chcete něco tak jednoduchého, jako je struktura, a nechcete platit náklady na výkon. CLR poskytuje možnost určit, co se nazývá ValueType, a v době kompilace je to považováno za strukturu. ValueTypes jsou spravovány zásobníkem a poskytují veškerou rychlost struktury. Podle očekávání mají také omezenou flexibilitu struktur (například neexistuje dědičnost). Ale pro instance, kde vše, co potřebujete, je struktura, ValueTypes poskytují neuvěřitelné zrychlení. Podrobnější informace o valueTypes a zbytku systému typů CLR jsou k dispozici v knihovně MSDN.
Ladění rychlosti
ValueTypes jsou užitečné pouze v případech, kdy je použijete jako struktury. Pokud potřebujete zacházet s ValueType jako s objektem, bude doba běhu zpracovávat boxování a rozbalování objektu za vás. To je však ještě dražší než jeho vytvoření jako objektu na prvním místě!
Tady je příklad jednoduchého testu, který porovnává dobu potřebnou k vytvoření velkého počtu objektů a hodnotových typů:
using System;
using System.Collections;
namespace ConsoleApplication{
public struct foo{
public foo(double arg){ this.y = arg; }
public double y;
}
public class bar{
public bar(double arg){ this.y = arg; }
public double y;
}
class Class1{
static void Main(string[] args){
Console.WriteLine("starting struct loop....");
int t1 = Environment.TickCount;
for (int i = 0; i < 25000000; i++) {
foo test1 = new foo(3.14);
foo test2 = new foo(3.15);
if (test1.y == test2.y) break; // prevent code from being
eliminated JIT
}
int t2 = Environment.TickCount;
Console.WriteLine("struct loop: (" + (t2-t1) + "). starting object
loop....");
t1 = Environment.TickCount;
for (int i = 0; i < 25000000; i++) {
bar test1 = new bar(3.14);
bar test2 = new bar(3.15);
if (test1.y == test2.y) break; // prevent code from being
eliminated JIT
}
t2 = Environment.TickCount;
Console.WriteLine("object loop: (" + (t2-t1) + ")");
}
Vyzkoušejte si to sami. Časová mezera je v řádu několika sekund. Teď upravíme program tak, aby doba běhu musí vyboxovat a rozbalit naši strukturu. Všimněte si, že výhody rychlosti použití ValueType úplně zmizely! Morální je, že valuetype se používají jen ve výjimečných situacích, kdy je nepoužíváte jako objekty. Na tyto situace je důležité si dát pozor, protože když je použijete správně, výkon je často extrémně vysoký.
using System;
using System.Collections;
namespace ConsoleApplication{
public struct foo{
public foo(double arg){ this.y = arg; }
public double y;
}
public class bar{
public bar(double arg){ this.y = arg; }
public double y;
}
class Class1{
static void Main(string[] args){
Hashtable boxed_table = new Hashtable(2);
Hashtable object_table = new Hashtable(2);
System.Console.WriteLine("starting struct loop...");
for(int i = 0; i < 10000000; i++){
boxed_table.Add(1, new foo(3.14));
boxed_table.Add(2, new foo(3.15));
boxed_table.Remove(1);
}
System.Console.WriteLine("struct loop complete.
starting object loop...");
for(int i = 0; i < 10000000; i++){
object_table.Add(1, new bar(3.14));
object_table.Add(2, new bar(3.15));
object_table.Remove(1);
}
System.Console.WriteLine("All done");
}
}
}
Microsoft používá valueType ve velkém smyslu: všechna primitiva v architekturách jsou ValueTypes. Moje doporučení je použít ValueTypes vždy, když máte pocit, že jste svědění pro strukturu. Pokud nebudete box / unbox, mohou poskytnout obrovské zvýšení rychlosti.
Je velmi důležité poznamenat, že ValueTypes nevyžadují ve scénářích spolupráce žádné zařazování. Vzhledem k tomu, že zařazování je jedním z největších výkonnostních hitů při spolupráci s nativním kódem, použití ValueTypes jako argumentů nativních funkcí je možná tou největší úpravou výkonu, kterou můžete udělat.
Další materiály
Mezi související témata týkající se výkonu v rozhraní .NET Framework patří:
Podívejte se na budoucí články, které jsou aktuálně ve vývoji, včetně přehledu návrhu, architektury a kódování, návodu k nástrojům pro analýzu výkonu ve spravovaném světě a porovnání výkonu platformy .NET s dalšími podnikovými aplikacemi, které jsou dnes k dispozici.
Příloha: Hostování doby běhu serveru
#include "mscoree.h"
#include "stdio.h"
#import "mscorlib.tlb" named_guids no_namespace raw_interfaces_only \
no_implementation exclude("IID_IObjectHandle", "IObjectHandle")
long main(){
long retval = 0;
LPWSTR pszFlavor = L"svr";
// Bind to the Run time.
ICorRuntimeHost *pHost = NULL;
HRESULT hr = CorBindToRuntimeEx(NULL,
pszFlavor,
NULL,
CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void **)&pHost);
if (SUCCEEDED(hr)){
printf("Got ICorRuntimeHost\n");
// Start the Run time (this also creates a default AppDomain)
hr = pHost->Start();
if(SUCCEEDED(hr)){
printf("Started\n");
// Get the Default AppDomain created when we called Start
IUnknown *pUnk = NULL;
hr = pHost->GetDefaultDomain(&pUnk);
if(SUCCEEDED(hr)){
printf("Got IUnknown\n");
// Ask for the _AppDomain Interface
_AppDomain *pDomain = NULL;
hr = pUnk->QueryInterface(IID__AppDomain, (void**)&pDomain);
if(SUCCEEDED(hr)){
printf("Got _AppDomain\n");
// Execute Assembly's entry point on this thread
BSTR pszAssemblyName = SysAllocString(L"Managed.exe");
hr = pDomain->ExecuteAssembly_2(pszAssemblyName, &retval);
SysFreeString(pszAssemblyName);
if (SUCCEEDED(hr)){
printf("Execution completed\n");
//Execution completed Successfully
pDomain->Release();
pUnk->Release();
pHost->Stop();
return retval;
}
}
pDomain->Release();
pUnk->Release();
}
}
pHost->Release();
}
printf("Failure, HRESULT: %x\n", hr);
// If we got here, there was an error, return the HRESULT
return hr;
}
Pokud máte dotazy nebo připomínky k tomuto článku, obraťte se na Claudio Caldato, programový manažer pro problémy s výkonem rozhraní .NET Framework.