Sam kompilator C # nie zmienia zbyt wiele emitowanej IL w kompilacji Release. Godne uwagi jest to, że nie emituje już kodów NOP, które pozwalają ustawić punkt przerwania na nawiasach klamrowych. Duży to optymalizator wbudowany w kompilator JIT. Wiem, że dokonuje następujących optymalizacji:
Metoda inliningu. Wywołanie metody jest zastępowane przez wstrzyknięcie kodu metody. Jest to duży, sprawia, że dostęp do nieruchomości jest zasadniczo bezpłatny.
Przydział rejestru procesora. Lokalne zmienne i argumenty metody mogą pozostać przechowywane w rejestrze procesora bez (lub rzadziej) przechowywania z powrotem do ramki stosu. Jest to duży problem, szczególnie utrudniający debugowanie zoptymalizowanego kodu. I nadając zmiennemu słowu kluczowemu znaczenie.
Eliminacja sprawdzania indeksu tablicy. Ważna optymalizacja podczas pracy z tablicami (wszystkie klasy kolekcji .NET używają tablicy wewnętrznie). Gdy kompilator JIT może sprawdzić, czy pętla nigdy nie indeksuje tablicy poza granicami, eliminuje to sprawdzanie indeksu. Duży.
Rozwijanie pętli. Pętle z małymi ciałami są ulepszane przez powtarzanie kodu do 4 razy w ciele i mniej zapętlania. Zmniejsza koszt oddziału i poprawia super-skalarne opcje wykonania procesora.
Eliminacja martwego kodu. Instrukcja taka jak if (false) {/ ... /} zostanie całkowicie wyeliminowana. Może się to zdarzyć z powodu ciągłego składania i wkładania. W innych przypadkach kompilator JIT może ustalić, że kod nie ma możliwego efektu ubocznego. Ta optymalizacja sprawia, że profilowanie kodu jest tak trudne.
Podnoszenie kodu. Kod wewnątrz pętli, na który pętla nie ma wpływu, można przenieść z pętli. Optymalizator kompilatora C poświęci znacznie więcej czasu na znalezienie okazji do podniesienia. Jest to jednak kosztowna optymalizacja ze względu na wymaganą analizę przepływu danych, a jitter nie może sobie pozwolić na czas, więc podnoszą tylko oczywiste przypadki. Zmuszanie programistów .NET do pisania lepszego kodu źródłowego i podnoszenia się.
Wspólna eliminacja podwyrażeń. x = y + 4; z = y + 4; staje się z = x; Dość powszechne w instrukcjach takich jak dest [ix + 1] = src [ix + 1]; napisane dla czytelności bez wprowadzania zmiennej pomocniczej. Nie trzeba zmniejszać czytelności.
Stałe składanie. x = 1 + 2; staje się x = 3; Ten prosty przykład został wcześnie wychwycony przez kompilator, ale dzieje się to w czasie JIT, kiedy inne optymalizacje umożliwiają to.
Kopiuj propagację. x = a; y = x; staje się y = a; Pomaga to alokatorowi rejestru podejmować lepsze decyzje. W jitteru x86 jest to wielka sprawa, ponieważ ma niewiele rejestrów do pracy. Wybór odpowiednich jest kluczowy dla perf.
Są to bardzo ważne optymalizacje, które mogą spowodować wielki kontrakt różnicy kiedy, na przykład, profil kompilacji Debug aplikacji i porównać go do kompilacji Release. To naprawdę ma znaczenie tylko wtedy, gdy kod znajduje się na twojej krytycznej ścieżce, 5 do 10% pisanego kodu, które faktycznie wpływa na perf twojego programu. Optymalizator JIT nie jest wystarczająco inteligentny, aby z góry wiedzieć, co jest najważniejsze, może zastosować tylko pokrętło „zamień na jedenaście” dla całego kodu.
Na efektywny wynik tych optymalizacji czasu wykonywania programu często wpływa kod działający w innym miejscu. Odczytywanie pliku, wykonywanie zapytania dbase itp. Optymalizacja JIT czyni pracę całkowicie niewidoczną. Nie przeszkadza to jednak :)
Optymalizator JIT jest dość niezawodnym kodem, głównie dlatego, że został przetestowany miliony razy. Bardzo rzadko występują problemy z wersją kompilacji wydania programu. Tak się jednak zdarza. Zarówno jitter x64, jak i x86 miały problemy ze strukturami. Jitter x86 ma problemy ze spójnością zmiennoprzecinkową, powodując nieznacznie różne wyniki, gdy półprodukty obliczeń zmiennoprzecinkowych są przechowywane w rejestrze FPU z 80-bitową precyzją, zamiast zostać obciętym po opróżnieniu do pamięci.