Tak naprawdę, moje posty powinienem rozpocząć od wyjaśnienia czym jest JIT. To jedno z podstawowych pojęć, które pojawia się w przypadku omawiania .NET. Wolałem jednak najpierw pokazać kilka programów napisanych w IL Assembly. Kilka postów powinno dać już jakiś obraz czym jest IL. Oczywiście kod piszemy w C# lub w innym języku wysokiego poziomu, więc wystarczy abyśmy ogólnie mieli pojęcie o IL.
Co to jest więc kompilacja JIT? Czym różni się od klasycznej? W językach niezarządzanych takich jak CPP, kompilator wygeneruje kod maszynowy. W świecie .NET kompilacja jest dwuetapowa. Używając Visual Studio nie generujemy kodu maszynowego, a właśnie IL – język pośredni, który na początku jest kompletnie bezużyteczny. Pomimo, że przypomina on kod maszynowy, to nie można go uruchomić na żadnej maszynie bez wcześniejszego przetworzenia.
Wspomniane “przetworzenie” to kolejna kompilacja, a mianowicie JIT (Just in Time). W większości przypadków odbywa się w czasie rzeczywistym, w momencie pierwszego wywołania danej metody. Nie JIT’ujemy całej aplikacji od razu bo byłoby to zbyt wolne. Jeśli w ogóle nie wywołamy jakieś metody, po prostu nie zostanie wygenerowany kod maszynowy dla niej. JIT zatem, to drugi etap kompilacji, który wygeneruje już konkretny kod maszynowy, bardzo często specyficzny dla konkretnej architektury. Ma to wiele zalet, takich jak niezależność od CPU i możliwość optymalizacji kodu dla konkretnej konfiguracji.
Naturalne jest, że JIT dodaje pewien overhead do aplikacji. Wywołanie każdej metody pierwszy raz jest wolniejsze niż kolejne. Z tego względu, jeśli piszemy jakieś benchmarki, musimy mieć to na uwadze, że wyniki mogą być sfałszowane przez JIT. Istnieje możliwość z JIT’owania całej aplikacji przed uruchomieniem programu, ale o tym kiedy indziej. Polecam również mój stary wpis o tzw. MultiCore JIT, który został dodany w .NET 4.5 i ma na celu przyśpieszenie JIT poprzez zrównoleglenie kompilacji.
Podkreślę jeszcze raz, że drugi etap komplikacji (JIT) w przeciwieństwie do pierwszego (Visual Studio, kompilator C#), nie jest wykonywany dla całej aplikacji, a wyłącznie dla konkretnej metody. Po prostu dzieje się to w czasie rzeczywistym i nie ma tyle czasu, aby analizować cały kod. JIT zatem nie dysponuje informacjami o całym kodzie i może wykonać optymalizacje dotyczące wyłącznie kawałka kodu ( w przeciwieństwie do komplikacji c#). Prawdziwa optymalizacja wynika jednak z faktu, że JIT bierze pod uwagę architekturę komputera (CPU) i może wygenerować instrukcje maszynowe , które teoretycznie powinny być optymalne.
Większość książek zawiera powyższe teoretyczne informacje i z tego względu, lepiej popraktykować tutaj. Posłużę się tutaj narzędziem windbg.exe. Jeśli nie pracowaliście z tym debuggerem to polecam MSDN. Windbg.exe jest bardzo przydatny w środowisku produkcyjnym, gdzie nie mamy kodu źródłowego i tym bardziej Visual Studio. Narzędzie jest dość trudne w obsłudze, ale przydatne w eksperymentowaniu oraz analizowaniu błędów, które zdarzyły się na produkcji (np. poprzez memory dump).
Nasz eksperyment przeprowadzimy na:
class Program { static void Main(string[] args) { Display("1"); Console.ReadLine(); Display("2"); } private static void Display(string text) { Console.WriteLine(text); } }
Wywołujemy tam dwukrotnie Display. Za pierwszym razem powinna zostać dokonana kompilacja JIT, a przy drugim wywołaniu, będziemy już dysponować kodem maszynowym.
Pierwszy etap kompilacji, który wygeneruje IL jest prosty i wykonujemy go np. bezpośrednio z Visual Studio. Poskutkuje to wygenerowanym plikiem EXE:
Windbg, który stanowi część Windows SDK, jest ogólnym narzędziem do debuggowania. Domyślnie nie wie nic o CLR i zarządzanym kodzie. Z tego względu, musimy zainstalować dodatkowe rozszerzenie SOS.DLL. Aby załadować SOS wystarczy wykonać polecenie:
.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll
Kolejna komenda to:
sxe ld:clrjit
Potrzebujemy jej, aby debugger został powiadomiony, gdy CLRJIT jest załadowany.
Następnie wpisujemy g, aby kontynuować:
g
Dzięki poprzedniemu poleceniu zobaczymy:
0:000> g (1134.17d8): Unknown exception - code 04242420 (first chance) ModLoad: 730f0000 7316d000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll eax=00000000 ebx=00800000 ecx=00000000 edx=00000000 esi=00000000 edi=7f06e000 eip=7721cfbc esp=00bce5dc ebp=00bce638 iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202 ntdll!NtMapViewOfSection+0xc: 7721cfbc c22800 ret 28h
Ustawmy breakpoint na metodzie Display za pomocą:
!bpmd ConsoleApplication12.exe ConsoleApplication12.Program.Main
Mamy dokładnie jedną metodę o takiej nazwie więc debugger wyświetli:
Found 1 methods in module 00cc2ed4... MethodDesc = 00cc37a8 Adding pending breakpoints...
00cc37a8 to identyfikator metody. Dzięki niemu możemy wyświetlić IL wpisując polecenie:
!dumpil 00cc37a8
Wynik:
IL_0000: nop IL_0001: ldstr "1" IL_0006: call ConsoleApplication12.Program::Display IL_000b: nop IL_000c: call System.Console::ReadLine IL_0011: pop IL_0012: ldstr "2" IL_0017: call ConsoleApplication12.Program::Display IL_001c: nop IL_001d: ret
Taki sam kod IL zobaczymy oczywiście w Reflector. Nie powinno być dla nas zaskoczeniem, że pierwszy etap kompilacji został wykonany – mamy w końcu EXE. Naszym celem jest udowodnienie, że JIT nie został jeszcze wykonany, ponieważ metoda nie została wywołana.
Kolejna komenda, wyświetli kod maszynowy:
!u 00cc37a8
Wynik:
00a30050 55 push ebp 00a30051 8bec mov ebp,esp 00a30053 83ec08 sub esp,8 00a30056 33c0 xor eax,eax 00a30058 8945f8 mov dword ptr [ebp-8],eax 00a3005b 894dfc mov dword ptr [ebp-4],ecx 00a3005e 833d78318e0000 cmp dword ptr ds:[8E3178h],0 00a30065 7405 je 00a3006c 00a30067 e8a4c2c673 call clr!JIT_DbgIsJustMyCode (7469c310) 00a3006c 90 nop c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 13: 00a3006d 8b0d88214603 mov ecx,dword ptr ds:[3462188h] ("1") 00a30073 ff15b0378e00 call dword ptr ds:[8E37B0h] (ConsoleApplication12.Program.Display(System.String), mdToken: 06000002) 00a30079 90 nop c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 14: 00a3007a e8b1a71b73 call mscorlib_ni+0xa0a830 (73bea830) (System.Console.ReadLine(), mdToken: 06000995) 00a3007f 8945f8 mov dword ptr [ebp-8],eax 00a30082 90 nop c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 15: 00a30083 8b0d8c214603 mov ecx,dword ptr ds:[346218Ch] ("2") 00a30089 ff15b0378e00 call dword ptr ds:[8E37B0h] (ConsoleApplication12.Program.Display(System.String), mdToken: 06000002) 00a3008f 90 nop c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 16: 00a30090 90 nop 00a30091 8be5 mov esp,ebp 00a30093 5d pop ebp 00a30094 c3 ret
Proszę zwrócić uwagę na wywołanie metody Display:
00a30073 ff15b0378e00 call dword ptr ds:[8E37B0h] (ConsoleApplication12.Program.Display(System.String), mdToken: 06000002)
Ustawiając teraz breakpoint na Display dostaniemy:
0:000> !bpmd ConsoleApplication12.exe ConsoleApplication12.Program.Display Found 1 methods in module 004f2ed4... MethodDesc = 004f37b4 Adding pending breakpoints...
Oznacza to, że 004f37b4 jest identyfikatorem Display. Spróbujmy wywołać !u 004f37b4, aby otrzymać kod:
0:000> !u 004f37b4 Not jitted yet
Widzimy wyraźnie, że jest to niemożliwe – drugi etap kompilacji Display nie nastąpił jeszcze. Jeśli pozwolimy na pierwsze wykonanie tej metody (polecenie “g”) to będziemy mogli zobaczyć kod za pomocą tego samego polecenia (!u 004f37b4):
005800d8 55 push ebp 005800d9 8bec mov ebp,esp 005800db 50 push eax 005800dc 894dfc mov dword ptr [ebp-4],ecx 005800df 833d78314f0000 cmp dword ptr ds:[4F3178h],0 005800e6 7405 je 005800ed 005800e8 e823c21174 call clr!JIT_DbgIsJustMyCode (7469c310) 005800ed 90 nop c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 24: 005800ee 8b4dfc mov ecx,dword ptr [ebp-4] 005800f1 e84601fa72 call mscorlib_ni+0x34023c (7352023c) (System.Console.WriteLine(System.String), mdToken: 060009a3) 005800f6 90 nop c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 25: 005800f7 90 nop 005800f8 8be5 mov esp,ebp 005800fa 5d pop ebp 005800fb c3 ret
Możemy również napisać mały benchmark, aby pokazać, że faktycznie zachodzi JIT:
class Program { static void Main(string[] args) { Stopwatch stopWatch = Stopwatch.StartNew(); Display("1"); Console.WriteLine(stopWatch.ElapsedTicks); stopWatch.Restart(); Display("2"); Console.WriteLine(stopWatch.ElapsedTicks); } private static void Display(string text) { Console.WriteLine(text); } }
Na ekranie pojawi się:
Widać, że drugie wywołanie jest wyraźnie szybsze ponieważ kod jest już po JIT.
Polecam zapoznanie się z narzędziem WinDbg, szczególnie dla osób, które muszą naprawiać błędy występujące wyłącznie na produkcji lub w innym specyficznym środowisku.
Wygodniejsze i bardziej uniwersalne jest .loadby sos clr. Wczytuje odpowiedniego SOS-a dla clr użytego we wczytanej do windbg aplikacji/dumpa.
Komentarze zamknięte? 🙂