× Aktuálně z oboru

Vychází Game Ready ovladače pro Far Cry 5 [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]
Celá zprávička [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]

Architektura Microsoft .NET Framework – 2. díl

[ http://programujte.com/profil/2482-stanislav-fajfr/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/118-zdenek-lehocky/ ]Google [ ?rel=author ]       12. 5. 2006       20 466×

  • Načítání assembly v CLR (Common Language Runtime)
  • JIT (Just In Time) kompilace

V minulém díle jsme si ukázali základní jednotku .NET Frameworku - assembly. V tomto díle si vysvětlíme, jak s nimi CLR pracuje.

Načítání assembly v CLR (Common Language Runtime)

Assembly je buď spustitelný soubor aplikace nebo DLL knihovna. CLR je samozřejmě zodpovědný za spuštění kódu obsaženém v assembly. Znamená to tedy, že na cílovém počítači musí být .NET Framework nainstalován. Microsoft proto vytvořil přenositelný balíček s touto platformou, který si můžete jednoduše nainstalovat. V budoucích Windows bude .NET Framework standardní součástí operačního systému.

Existuje jednoduchý způsob, jak zjistit, zda-li je .NET Framework nainstalován. Pokud naleznete v adresáři %windir%\system32 soubor MSCorEE.dll (Component Object Runtime Execution Engine), nainstalován je. Je také důležité si uvědomit, že lze nainstalovat několik verzí .NET Frameworku současně. Verzi platformy naleznete v registrech HKEY_LOCAL_MECHAINE\SOFTWARE\Microsoft\.NETFramework\policy.

Když kompilátor vytváří assembly, vloží do PE hlavičky a do .text sekce speciální informace. Díky nim je při načtení assembly inicializován CLR, který najde vstupní metodu a aplikaci spustí.

Následující schéma ukazuje, jak CLR načítá assembly:

Když kompilátor vytvoří spustitelný soubor, vloží do .text sekce PE hlavičky 6 bajtů x86 instrukce:

JMP _CorExeMain

Fukce _CorExeMain je importována z dynamické knihovny Microsoft MSCorEE.dll, z toho důvodu se v sekci .idata nachází na ni reference. Při spuštění řízeného EXE souboru Windows postupují stejně jako u normálních (nativních) EXE souborů: Windows načtou soubor a prozkoumají sekci .idata, kde zjistí, že mají načíst MSCorEE.dll do paměti. Dále získají adresu funkce _CorExeMain, která se nachází v MSCorEE.dll knihovně. Instrukcí JMP ji pak spustíme.

Funkce _CorExeMain inicializuje CLR a podívá se do CLR hlavičky spustitelného souboru, kde má hledat vstupní metodu (Main). Dále je IL kód této metody zkompilován do nativních CPU instrukcí a CLR ho spustí. V této chvíli běží řízený kód.

Pro řízené DLL knihovny je situace podobná. Při kompilace vloží kompilátor 6 bajtů do sekce .text v hlavičce PE souboru:

JMP _CorDllMain

Funkce _CorDllMain je také importována z knihovny MSCorEE.dll pomocí .idata sekce řízené knihovny. Když Windows načtou tuto řízenou DLL, automaticky načtou MSCorEE.dll (pokud již není načtena), zjistí adresu funkce _CorDllMain a instrukcí JMP ji spustí. _CorDllMain pak inicializuje CLR (pokud se tak již nestalo) a předá řízení programu.

Výše uvedená 6 bajtová instrukce je požadována pro běh assembly na Windows 98, 98SE, ME, NT 4 a Windows 2000, protože tyto operační systémy byly vyvinuty o mnoho dříve než .NET Framework. Zapamatujte si, že tato 6 bajtová instrukce platí pouze pro x86 platformu. Tato instrukce nebude fungovat na jiných CPU architekturách. Jelikož Windows XP a Windows.NET Server podporují x86 a IA64 CPU architekturu, loadery těchto operačních systémů byly modifikovány pro přímou manipulaci s assembly.

Když je ve Windows XP nebo Windows.NET Server volána assembly (typicky přes CreateProcess nebo LoadLibrary), operační systém sám detekuje řízený kód a načte MSCorEE.dll knihovnu do paměti. Šestibajtová instrukce je pak zcela ignorována.

Poslední poznámka k řízeným PE souborům: Řízené PE soubory používají vždy 32 bitový PE formát, nikdy nepoužívají 64 bitový formát. V 64 bitových Windows loader operačního systému detekuje řízený 32 bitový PE soubor a ví, že má vytvořit 64 bitové adresování.


JIT (Just In Time) kompilace

Jak bylo zmiňováno dříve, řízené moduly obsahují metadata a IL (intermediate) kód. IL je nezávislý strojový jazyk vytvořený společností Microsoft po konzultaci s několika externímy a akademickými programátory překladačů, tvůrci programovacích jazyků. Jedná se o objektově orientovaný strojový jazyk. IL rozumí typům, obsahuje instrukce k vytváření a inicializaci objektů, může volat virtuální metody a přímo manipulovat s elementy polí. Dokonce má instrukce pro vytváření a zachytávání výjimek.

Programátoři ovšem dávají přednost vysokoúrovňovým jazykům jako C# nebo Visual Basic.NET. Kompilátory všech těchto vysokoúrovňových jazyků produkují IL kód. Stejně jako u ostatních strojových jazyků i zde má programátor možnost psát přímo v tomto jazyce. Microsoft proto poskytuje IL assembler ILAsm.exe a disassembler ILDasm.exe.

Instrukce v jazyku IL nemohou být zpracovávány přímo v CPU (i když i to se může v budoucnosti změnit). V případě spuštění metody je její IL kód nejprve převeden do nativních CPU instrukcí. Pro tuto konverzi poskytuje CLR JIT(Just In Time) kompilátor.

Na tomto schématu je zobrazeno, co se stane v případě prvního spuštění metody:

Než je spuštěna metoda Main, CLR detekuje všechny typy, na které je v metodě odkazováno. Dále alokuje interní datovou strukturu, kterou použije k řízenému přístupu ke všem odkazovaným typům. Ve schématu nahoře metoda Main odkazuje na jeden typ Console, CLR tedy alokuje jednu interní strukturu, která obsahuje adresy ke všem metodám definovaných v této třídě (Console). Když CLR inicializuje tuto strukturu, nastaví adresy všech metod na funkci nacházející se v samotném CLR. Tuto funkci si nazveme JITCompiler.

Při prvním volání metody WriteLine je volána funkce JITCompiler, která je zodpovědná za zkompilování IL kódu metody WriteLine do nativních CPU instrukcí.

Při volání metody WriteLine JITCompiler věděl, jak se metoda nazývá a na jaké typy odkazuje. Tato funkce si totiž předtím našla metadata příslušné assembly. Zkompilovala IL kód do nativních CPU instrukcí a uložila jej do dynamicky alokovaného bloku v paměti. JITCompiler se vrátí zpět k interní datové struktuře a upraví adresu metody WriteLine na adresu nativní verze v paměti. Nakonec JITCompiler spustí nativní kód metody WriteLine (verze s parametrem String). Po vykonání kódu se řízení vrací zpět do metody Main.

Uvažujme nyní, že metoda Main volá metodu WriteLine podruhé. Nyní, jelikož byl kód této metody již zkompilován, volání směřuje přímo do nativního kódu v paměi, při čemž se funkce JITCompiler jednoduše přeskočí. Po skončení metody WriteLine se řízení předá zpět do metody Main.

Následující schéma zobrazuje situaci při druhém volání metody WriteLine:

K nárůstu spotřeby výkonu dochází pouze při prvním volání metody. Každé další spuštění metody již není ničím zdržováno a běží plnou rychlostí nativního kódu.

JIT kompilátor ukládá nativní CPU instrukce do paměti. Tento kód je odstraněn z paměti, jakmile dojde k ukončení příslušné aplikace. Takže když spustíte aplikaci někdy v budoucnu nebo běží dvě instance aplikace současně (ve dvou oddělených procesech), JIT kompilátor bude kompilovat IL kód do nativních instrukcí znovu.

Pro většinu aplikací není úbytek výkonu způsobený JIT kompilací významný. Většina aplikací má sklony volat stejnou metodu znovu a znovu, takže tyto metody budou pomalejší pouze při spuštění aplikace. Navíc, proces ušetří více výkonu uvnitř metody než jejím voláním.

Je třeba si uvědomit, že JIT kompilátor optimalizuje nativní kód stejně jako kompilátor neřízeného C++. Optimalizace možná zabere trochu času, ale kód bude mnohem výkonnější než neoptimalizovaný.

Vývojáři pracující v neřízeném C nebo C++ si budou zřejmě myslet své. Konec konců, neřízený kód je zkompilován pro specifickou CPU platformou a když je volán, kód je jednoduše spuštěn. Naproti tomu, v řízeném prostředí je kompilace kódu rozdělena do dvou částí. V první kompilátor převede zdrojový kód na IL strojový kód s maximální možnou efektivitou. Později je IL zkompilován do nativních CPU instrukcí, což vyžaduje více operační paměti a CPU výkonu.

Věřte mi, takový přístup jsem také zastával. Pravda je, že kompilace nepatrně snižuje výkon a alokuje operační paměť. Na druhou stranu Microsoft udělal hodně práce na tom, aby byly výkonnostní ztráty minimální.

Pokud jste stále skeptičtí, měli byste si vytvořit nějakou aplikaci a otestovat ji. Pak byste si mohli vyzkoušet nějakou netriviální řízenou aplikaci od Microsoftu a změřit její výkonnost. Myslím, že byste byli překvapeni, jak dobrých výsledků dosáhne.

Ve skutečnosti, i když je těžké tomu uvěřit, může být řízená aplikace výkonnější než nativní. Například když JIT kompiluje IL kód do nativních CPU instrukcí, zná o prostředí (počítači) mnohem více informací než by mohl kompilátor nativních aplikací kdy znát.

Zde je několik cest, jak by mohl být řízený kód výkonnější než nativní:

  • JIT kompilátor detekuje, zda-li aplikace běží na Pentium 4 a produkuje pak nativní kód s některými speciálními instrukcemi určenými pouze pro Pentium 4.
  • Například, když metoda obsahuje tento kód
    
    if(numberOfCPUs > 1) {
    }
    

    nemusí na jednoprocesorových počítačích generovat žádný kód. Kód pak bude menší a rychlejší.

Z těchto a mnoha dalších důvodů můžeme v budoucnosti čekat lepší výkonnost.

Jestliže jste přišli na to, že vám JIT kompilátor neposkytuje dostatek výkonu, mohli byste ocenit nástroj NGen.exe [ http://msdn2.microsoft.com/en-us/library/6t9t5wcf.aspx ] , který je součástí .NET Framework SDK. Tento nástroj zkompiluje celou assembly do nativního kódu a uloží ho na disk. Při načtení assembly pak CLR zkontroluje, jestli existuje předkompilovaná verze. Pokud ano, načte ji, a pokud ne, kompiluje ji JIT kompilátor jako obvykle.

Co nás čeká přístě

Příště si podrobně popíšeme, jak pracuje Garbage Collection, neboli automatická správa paměti v .NET Frameworku


Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2006042902-architektura-microsoft-net-framework-2-dil/ ].