Kompilacja C # JIT i .NET

86

Trochę się pogubiłem, jeśli chodzi o szczegóły działania kompilatora JIT. Wiem, że C # kompiluje się do IL. Przy pierwszym uruchomieniu jest to JIT. Czy wiąże się to z tłumaczeniem na kod natywny? Czy środowisko wykonawcze .NET (jako maszyna wirtualna?) Współdziała z kodem JIT? Wiem, że to naiwne, ale naprawdę się zagubiłem. Zawsze miałem wrażenie, że zestawy nie są interpretowane przez środowisko wykonawcze .NET, ale nie rozumiem szczegółów interakcji.

Steve
źródło

Odpowiedzi:

83

Tak, kod IL w JIT wymaga tłumaczenia IL na natywne instrukcje maszynowe.

Tak, środowisko wykonawcze .NET współdziała z natywnym kodem maszynowym JIT w tym sensie, że środowisko wykonawcze jest właścicielem bloków pamięci zajmowanych przez natywny kod maszynowy, wywołania środowiska wykonawczego do natywnego kodu maszynowego itp.

Masz rację, że środowisko uruchomieniowe .NET nie interpretuje kodu IL w zestawach.

Dzieje się tak, gdy wykonanie osiąga funkcję lub blok kodu (na przykład klauzulę else bloku if), który nie został jeszcze skompilowany JIT do natywnego kodu maszynowego, JIT'r jest wywoływany w celu skompilowania tego bloku IL do natywnego kodu maszynowego . Kiedy to się stanie, wykonanie programu wprowadza świeżo wyemitowany kod maszynowy, aby wykonać swoją logikę programu. Jeśli podczas wykonywania tego natywnego kodu maszynowego dochodzi do wywołania funkcji, która nie została jeszcze skompilowana do kodu maszynowego, JIT'r jest wywoływany w celu skompilowania tej funkcji „w samą porę”. I tak dalej.

JIT'r niekoniecznie kompiluje całą logikę ciała funkcji do kodu maszynowego na raz. Jeśli funkcja ma instrukcje if, bloki instrukcji klauzul if lub else nie mogą być kompilowane w JIT, dopóki wykonanie faktycznie nie przejdzie przez ten blok. Ścieżki kodu, które nie zostały wykonane, pozostają w postaci IL, dopóki nie zostaną wykonane.

Skompilowany natywny kod maszynowy jest przechowywany w pamięci, dzięki czemu można go użyć ponownie przy następnym wykonaniu tej sekcji kodu. Przy drugim wywołaniu funkcji będzie ona działać szybciej niż przy pierwszym wywołaniu, ponieważ za drugim razem żaden krok JIT nie jest konieczny.

W środowisku .NET natywny kod maszynowy jest przechowywany w pamięci przez cały okres istnienia domeny aplikacji. W .NET CF natywny kod maszynowy może zostać wyrzucony, jeśli aplikacja ma mało pamięci. Zostanie on skompilowany ponownie w JIT z oryginalnego kodu IL, gdy następnym razem wykonanie przejdzie przez ten kod.

dthorpe
źródło
14
Drobna poprawka pedantyczna - „JIT” niekoniecznie kompiluje całą logikę ciała funkcji - w teoretycznej implementacji, która może mieć miejsce, ale w istniejących implementacjach środowiska uruchomieniowego .NET kompilator JIT skompiluje całość wszystkie metody naraz.
Paul Alexander
18
Jeśli chodzi o powód, dla którego tak się dzieje, dzieje się tak głównie dlatego, że kompilator non-jit nie może założyć, że pewne optymalizacje są dostępne na dowolnej platformie docelowej. Jedyne opcje to kompilacja do najniższego wspólnego mianownika lub, częściej, kompilacja wielu wersji, z których każda jest przeznaczona na własną platformę. JIT eliminuje tę wadę, ponieważ ostateczne tłumaczenie na kod maszynowy odbywa się na maszynie docelowej, na której kompilator wie, jakie optymalizacje są dostępne.
Chris Shain,
1
Chris, nawet sądząc, że JIT wie, jakie optymalizacje są dostępne, nie ma czasu na zastosowanie żadnych znaczących optymalizacji. Prawdopodobnie NGEN jest potężniejszy.
Grigorij
2
@Grigory Tak, NGEN ma luksus czasu, którego nie ma kompilator JIT, ale istnieje wiele decyzji dotyczących kodowania, które JIT może podjąć, aby poprawić wydajność skompilowanego kodu, których rozwiązanie nie zajmuje dużo czasu. Na przykład kompilator JIT może wybrać zestawy instrukcji, aby najlepiej pasowały do ​​dostępnego sprzętu znalezionego w czasie wykonywania, bez dodawania znacznego czasu do kroku kompilacji JIT. Wydaje mi się, że .NET JITter nie robi tego w znaczący sposób, ale jest to możliwe.
dthorpe
24

Kod jest „kompilowany” do języka pośredniego firmy Microsoft, który jest podobny do formatu asemblera.

Po dwukrotnym kliknięciu pliku wykonywalnego system Windows ładuje się, mscoree.dlla następnie konfiguruje środowisko CLR i uruchamia kod programu. Kompilator JIT rozpoczyna odczytywanie kodu MSIL w programie i dynamicznie kompiluje kod do instrukcji x86, które procesor może wykonać.

user541686
źródło
1

.NET używa języka pośredniego zwanego MSIL, czasami w skrócie IL. Kompilator odczytuje kod źródłowy i tworzy plik MSIL. Po uruchomieniu programu kompilator .NET Just In Time (JIT) odczytuje kod MSIL i tworzy w pamięci aplikację wykonywalną. Nic z tego nie zobaczysz, ale dobrze jest wiedzieć, co dzieje się za kulisami.

Gagan
źródło
1

Opiszę kompilację kodu IL do natywnych instrukcji CPU na przykładzie poniżej.

public class Example 
{
    static void Main() 
    {
        Console.WriteLine("Hey IL!!!");
    }
}

Przede wszystkim CLR zna wszystkie szczegóły dotyczące typu i metody wywoływanej z tego typu, co wynika z metadanych.

Gdy CLR zaczyna wykonywać IL w natywnej instrukcji procesora, CLR przydziela wewnętrzne struktury danych dla każdego typu, do którego odwołuje się kod Main.

W naszym przypadku mamy tylko jeden typ konsoli, więc CLR przydzieli jedną wewnętrzną strukturę danych poprzez tę wewnętrzną strukturę, będziemy zarządzać dostępem do przywoływanych typów

wewnątrz tej struktury danych CLR zawiera wpisy dotyczące wszystkich metod zdefiniowanych przez ten typ. Każdy wpis zawiera adres, pod którym można znaleźć implementację metody.

Podczas inicjowania tej struktury CLR ustawia każdy wpis w nieudokumentowanej FUNKCJI zawartej w samym CLR i, jak można się domyślić, ta FUNKCJA jest tym, co nazywamy JIT Compiler.

Ogólnie rzecz biorąc, można rozważyć JIT Compiler jako funkcję CLR, która kompiluje IL do natywnych instrukcji procesora. Pozwólcie, że pokażę wam szczegółowo, jak będzie wyglądał ten proces na naszym przykładzie.

1. Gdy Main wykonuje swoje pierwsze wywołanie WriteLine, wywoływana jest funkcja JITCompiler.

2. Funkcja kompilatora JIT wie, jaka metoda jest wywoływana i jaki typ definiuje tę metodę.

3. Następnie Jit Compiler przeszukuje zestaw, w którym zdefiniowano ten typ i pobiera kod IL dla metody zdefiniowanej przez ten typ w naszym przypadku kod IL metody WriteLine.

Kompilator JIT przydziela DYNAMICZNY blok pamięci, po czym JIT weryfikuje i kompiluje kod IL do natywnego kodu procesora i zapisuje ten kod procesora w tym bloku pamięci.

Następnie kompilator JIT wraca do wpisu wewnętrznej struktury danych i zastępuje adres (który odnosi się głównie do implementacji kodu IL w WriteLine) adresem nowym dynamicznie utworzonym blokiem pamięci, który zawiera natywne instrukcje CPU WriteLine.

Na koniec funkcja kompilatora JIT przeskakuje do kodu w bloku pamięci. Ten kod jest implementacją metody WriteLine.

7. Po zaimplementowaniu WriteLine, kod powraca do kodu Mains, który kontynuuje wykonywanie normalnie.

So_oP
źródło
0

NET Framework używa środowiska CLR do tworzenia MSIL (Microsoft Intermediate Language), nazywanego również IL. Kompilator odczytuje kod źródłowy, a kiedy budujesz / kompilujesz projekt, generuje MSIL. Teraz, gdy w końcu uruchomisz swój projekt, zacznie działać .NET JIT ( kompilator just -in-time ). JIT czyta twój kod MSIL i tworzy natywny kod (który jest instrukcją x86), który może być łatwo wykonany przez CPU. JIT czyta wszystkie instrukcje MSIL i wykonuje je linia po linii.

Jeśli chcesz zobaczyć, co dzieje się za kulisami, to już odpowiedź. Proszę śledzić - tutaj

Dikshit Kathuria
źródło