Natknąłem się na wiele wskazówek dotyczących optymalizacji, które mówią, że powinieneś oznaczyć swoje zajęcia jako zamknięte, aby uzyskać dodatkowe korzyści z wydajności.
Przeprowadziłem kilka testów, aby sprawdzić różnicę wydajności i nie znalazłem żadnego. czy robię coś źle? Czy brakuje mi przypadku, w którym zajęcia zamknięte dadzą lepsze wyniki?
Czy ktoś przeprowadził testy i zauważył różnicę?
Pomóż mi się uczyć :)
.net
optimization
frameworks
performance
Vaibhav
źródło
źródło
Odpowiedzi:
JITter czasami używa niewirtualnych wywołań metod w klasach zapieczętowanych, ponieważ nie ma możliwości ich dalszego rozszerzania.
Istnieją złożone zasady dotyczące typu wywołania, wirtualne / nie wirtualne, i nie znam ich wszystkich, więc nie mogę ich dla ciebie nakreślić, ale jeśli wyszukasz w Google zapieczętowane klasy i metody wirtualne, możesz znaleźć artykuły na ten temat.
Zwróć uwagę, że każdy rodzaj korzyści w zakresie wydajności, jaki można uzyskać z tego poziomu optymalizacji, należy traktować jako ostateczność, zawsze optymalizuj na poziomie algorytmicznym przed optymalizacją na poziomie kodu.
Oto jeden link, który o tym wspomina: wędrowanie po zapieczętowanym słowie kluczowym
źródło
Odpowiedź brzmi: nie, klasy zamknięte nie działają lepiej niż nie zamknięte.
Problem sprowadza się do kodów operacyjnych
call
vscallvirt
IL.Call
jest szybszy niżcallvirt
icallvirt
jest używany głównie wtedy, gdy nie wiesz, czy obiekt został poddany podklasie. Więc ludzie zakładają, że jeśli zapieczętujesz klasę, wszystkie kody operacyjne zmienią się zcalvirts
nacalls
i będą szybsze.Niestety
callvirt
robi też inne rzeczy, które czynią go użytecznym, na przykład sprawdzanie zerowych odwołań. Oznacza to, że nawet jeśli klasa jest zapieczętowana, odwołanie może nadal mieć wartość null, a zatem plikcallvirt
jest potrzebne. Możesz to obejść (bez konieczności pieczętowania klasy), ale staje się to trochę bezcelowe.Wykorzystanie struktur
call
ponieważ nie mogą być podklasy i nigdy nie są puste.Zobacz to pytanie, aby uzyskać więcej informacji:
Zadzwoń i zadzwoń
źródło
call
jest używany, to: w sytuacjinew T().Method()
, dlastruct
metod, dla niewirtualnych wywołańvirtual
metod (takich jakbase.Virtual()
) lub dlastatic
metod. Wszędzie indziej używacallvirt
.Aktualizacja: od .NET Core 2,0 i .NET Desktop 4.7.1 środowisko CLR obsługuje teraz dewirtualizację. Może przyjmować metody w klasach zapieczętowanych i zastępować wywołania wirtualne wywołaniami bezpośrednimi - i może to również robić dla klas niezamkniętych, jeśli zdoła ustalić, że jest to bezpieczne.
W takim przypadku (klasa zapieczętowana, której CLR nie mógłby w inny sposób wykryć jako bezpiecznej do dewirtualizacji), klasa zapieczętowana powinna w rzeczywistości oferować jakąś korzyść dotyczącą wydajności.
To powiedziawszy, nie sądzę, że warto się tym martwić, chyba że że wcześniej sprofilowałeś kod i ustaliłeś, że jesteś na szczególnie gorącej ścieżce, którą nazywają miliony razy, lub coś takiego:
https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/
Oryginalna odpowiedź:
Zrobiłem następujący program testowy, a następnie zdekompilowałem go za pomocą Reflectora, aby zobaczyć, jaki kod MSIL został wyemitowany.
We wszystkich przypadkach kompilator C # (Visual Studio 2010 w konfiguracji kompilacji wydania) emituje identyczny plik MSIL, który jest następujący:
Często cytowanym powodem, dla którego ludzie twierdzą, że Seal zapewnia korzyści w zakresie wydajności, jest to, że kompilator wie, że klasa nie jest nadpisana, i dlatego może używać
call
zamiastcallvirt
ponieważ nie musi sprawdzać wirtualności itp. Jak udowodniono powyżej, nie jest to prawdziwe.Następną moją myślą było to, że chociaż MSIL jest identyczny, być może kompilator JIT traktuje zapieczętowane klasy inaczej?
Uruchomiłem kompilację wydania w debugerze programu Visual Studio i obejrzałem zdekompilowane wyjście x86. W obu przypadkach kod x86 był identyczny, z wyjątkiem nazw klas i adresów pamięci funkcji (które oczywiście muszą być różne). Tutaj jest
Pomyślałem wtedy, że może uruchomienie debugera powoduje, że wykonuje on mniej agresywną optymalizację?
Następnie uruchomiłem samodzielny plik wykonywalny kompilacji wydania poza jakimikolwiek środowiskami debugowania i użyłem WinDBG + SOS, aby włamać się po zakończeniu programu i wyświetlić dezasemblację skompilowanego przez JIT kodu x86.
Jak widać z poniższego kodu, podczas uruchamiania poza debugerem kompilator JIT jest bardziej agresywny i wbudował
WriteIt
metodę bezpośrednio w obiekt wywołujący. Najważniejsze jest jednak to, że było to identyczne podczas wywoływania klasy zamkniętej i nieopieczętowanej. Nie ma żadnej różnicy między klasą zapieczętowaną i nieuszczelnioną.Oto on, dzwoniąc do zwykłej klasy:
Vs klasa szczelna:
Dla mnie jest to solidny dowód na to, że nie może być żadnej poprawy wydajności między wywoływaniem metod w klasach zamkniętych i niezamkniętych ... Myślę, że teraz jestem zadowolony :-)
źródło
callvirt
skoro te metody nie są wirtualne?callvirt
do zapieczętowanych metod, ponieważ nadal musi sprawdzać obiekt null przed wywołaniem wywołania metody, a kiedy to uwzględnisz, równie dobrze możesz po prostu używaćcallvirt
. Aby usunąćcallvirt
i po prostu przeskoczyć bezpośrednio, musieliby albo zmodyfikować C #, aby zezwolić((string)null).methodCall()
tak, jak robi to C ++, albo musieliby statycznie udowodnić, że obiekt nie jest pusty (co mogliby zrobić, ale nie przejmowali się)Jak wiem, nie ma żadnej gwarancji wydajności. Istnieje jednak szansa na zmniejszenie spadku wydajności pod pewnymi warunkami dzięki zastosowaniu metody szczelnej. (klasa zapieczętowana powoduje zapieczętowanie wszystkich metod).
Ale to zależy od implementacji kompilatora i środowiska wykonawczego.
Detale
Wiele nowoczesnych procesorów wykorzystuje długą strukturę potoków w celu zwiększenia wydajności. Ponieważ procesor jest niewiarygodnie szybszy niż pamięć, procesor musi wstępnie pobrać kod z pamięci, aby przyspieszyć potok. Jeśli kod nie jest gotowy we właściwym czasie, potoki będą bezczynne.
Istnieje duża przeszkoda zwana dynamiczną wysyłką, która zakłóca optymalizację „wstępnego pobierania”. Można to rozumieć jako rozgałęzienie warunkowe.
W tym przypadku procesor nie może pobrać z wyprzedzeniem następnego kodu do wykonania, ponieważ kolejna pozycja kodu jest nieznana, dopóki warunek nie zostanie rozwiązany. Więc to stwarza zagrożenie powoduje bezczynność rurociągu. W normalnych warunkach spadek wydajności spowodowany bezczynnością jest ogromny.
Podobnie dzieje się w przypadku nadpisywania metody. Kompilator może określić odpowiednie przesłanianie metody dla bieżącego wywołania metody, ale czasami jest to niemożliwe. W takim przypadku właściwą metodę można określić tylko w czasie wykonywania. Jest to również przypadek dynamicznego wysyłania, a głównym powodem, dla którego języki dynamicznie typowane są generalnie wolniej niż języki statyczne.
Niektóre procesory (w tym najnowsze układy Intel x86) wykorzystują technikę zwaną wykonywaniem spekulatywnym, aby wykorzystać potok nawet w danej sytuacji. Wystarczy pobrać wstępnie jedną ze ścieżek wykonywania. Ale wskaźnik trafień tej techniki nie jest tak wysoki. A niepowodzenie spekulacji powoduje zastój w rurociągu, co również powoduje ogromny spadek wydajności. (jest to całkowicie spowodowane implementacją procesora. Niektóre procesory mobilne są znane jako brak tego rodzaju optymalizacji w celu oszczędzania energii)
Zasadniczo C # jest językiem kompilowanym statycznie. Ale nie zawsze. Nie znam dokładnego warunku i zależy to wyłącznie od implementacji kompilatora. Niektóre kompilatory mogą wyeliminować możliwość dynamicznego wysyłania, zapobiegając zastępowaniu metody, jeśli metoda jest oznaczona jako
sealed
. Głupie kompilatory nie mogą. To jest korzyść z wydajności programusealed
.Ta odpowiedź ( Dlaczego posortowana tablica jest szybsza niż nieposortowana? ) Znacznie lepiej opisuje prognozowanie rozgałęzienia.
źródło
<off-topic-rant>
ja nienawidzę zapieczętowanych klas. Nawet jeśli korzyści w zakresie wydajności są zdumiewające (w co wątpię), niszczą one model zorientowany obiektowo, uniemożliwiając ponowne użycie poprzez dziedziczenie. Na przykład klasa Thread jest zapieczętowana. Chociaż widzę, że można chcieć, aby wątki były tak wydajne, jak to tylko możliwe, mogę również wyobrazić sobie scenariusze, w których możliwość podklasy wątku przyniosłaby ogromne korzyści. Autorzy klas, jeśli musicie zapieczętować swoje zajęcia ze względu na „wydajność”, prosimy o zapewnienie przynajmniej interfejsu , abyśmy nie musieli zawijać i zastępować wszędzie tam, gdzie potrzebujemy funkcji, o której zapomnieliście.
Przykład: SafeThread musiał opakować klasę Thread, ponieważ Thread jest zapieczętowany i nie ma interfejsu IThread; SafeThread automatycznie przechwytuje nieobsłużone wyjątki w wątkach, czego całkowicie brakuje w klasie Thread. [i nie, nieobsłużone zdarzenia wyjątków nie pobierają nieobsłużonych wyjątków w wątkach pomocniczych].
</off-topic-rant>
źródło
Oznaczanie zajęć
sealed
powinno mieć wpływu na wydajność.Istnieją przypadki, w których
csc
może być konieczne wyemitowaniecallvirt
kodu operacyjnego zamiastcall
kodu operacyjnego. Wydaje się jednak, że takie przypadki są rzadkie.Wydaje mi się, że JIT powinien być w stanie wyemitować to samo niewirtualne wywołanie funkcji
callvirt
, co by zrobiłcall
, jeśli wie, że klasa nie ma (jeszcze) żadnych podklas. Jeśli istnieje tylko jedna implementacja metody, nie ma sensu ładować jej adresu z tabeli vtable - po prostu wywołaj bezpośrednio jedną implementację. W tym przypadku JIT może nawet wbudować funkcję.To trochę ryzykowne ze strony JIT, ponieważ jeśli podklasa jest później załadowana, JIT będzie musiał wyrzucić ten kod maszynowy i ponownie skompilować kod, emitując prawdziwe wirtualne wywołanie. Domyślam się, że w praktyce nie zdarza się to często.
(I tak, projektanci maszyn wirtualnych naprawdę agresywnie dążą do tych niewielkich korzyści w zakresie wydajności).
źródło
Klasy zamknięte powinny zapewnić poprawę wydajności. Ponieważ nie można wyprowadzić zapieczętowanej klasy, dowolne wirtualne elementy członkowskie można przekształcić w niewirtualne elementy członkowskie.
Oczywiście mówimy o naprawdę małych zyskach. Nie oznaczałbym klasy jako zapieczętowanej tylko po to, aby uzyskać poprawę wydajności, chyba że profilowanie ujawniło, że jest to problem.
źródło
call
zamiastcallvirt
... Chciałbym również niezerowe typy referencyjne z wielu innych powodów ... westchnienie :-(Uważam „zapieczętowane” klasy za normalny przypadek i ZAWSZE mam powód, aby pomijać słowo kluczowe „zamknięte”.
Najważniejsze dla mnie powody to:
a) Lepsze sprawdzanie czasu kompilacji (rzutowanie na interfejsy, które nie zostały zaimplementowane, zostanie wykryte w czasie kompilacji, nie tylko w czasie wykonywania)
i główny powód:
b) Nadużywanie moich zajęć nie jest w ten sposób możliwe
Chciałbym, żeby Microsoft uczynił standard „zapieczętowanym”, a nie „nieopieczętowanym”.
źródło
@Vaibhav, jakie testy wykonałeś, aby zmierzyć wydajność?
Myślę, że należałoby użyć Rotora i zagłębić się w CLI i zrozumieć, w jaki sposób uszczelniona klasa poprawiłaby wydajność.
źródło
zapieczętowane klasy będą przynajmniej odrobinę szybsze, ale czasami mogą być szybsze ... jeśli JIT Optimizer może wbudować wywołania, które w innym przypadku byłyby wywołaniami wirtualnymi. Tak więc w przypadku często nazywanych metod, które są wystarczająco małe, aby można je było wstawić, zdecydowanie rozważ uszczelnienie klasy.
Jednak najlepszym powodem do zapieczętowania klasy jest powiedzenie „Nie zaprojektowałem tego, aby być dziedziczonym, więc nie pozwolę ci się spalić, zakładając, że tak zostało zaprojektowane, i nie zamierzam spalić się, zamykając się w implementacji, ponieważ pozwalam ci z niej czerpać. "
Wiem, że niektórzy tutaj powiedzieli, że nienawidzą klas zapieczętowanych, ponieważ chcą mieć możliwość czerpania z czegokolwiek ... ale to CZĘSTO nie jest to najbardziej łatwy w utrzymaniu wybór ... ponieważ wystawienie klasy na wyprowadzenie blokuje cię o wiele bardziej niż nie ujawnianie wszystkiego że. Jest to podobne do powiedzenia „Nienawidzę zajęć, które mają prywatnych członków… Często nie mogę zmusić ich do zrobienia tego, co chcę, ponieważ nie mam dostępu”. Hermetyzacja jest ważna ... uszczelnianie jest jedną z form hermetyzacji.
źródło
callvirt
(wywołanie metody wirtualnej) dla metod wystąpień w klasach zapieczętowanych, ponieważ nadal musi wykonać na nich sprawdzenie obiektów null. Jeśli chodzi o wstawianie, CLR JIT może (i robi) wbudowaną metodę wirtualną zarówno dla klas zapieczętowanych, jak i niezamkniętych ... więc tak. Wydajność to mit.Aby naprawdę je zobaczyć, musisz przeanalizować kod e skompilowany JIT (ostatni).
Kod C #
Kod MIL
JIT - skompilowany kod
Podczas gdy tworzenie obiektów jest takie samo, instrukcje wykonywane w celu wywołania metod klasy zapieczętowanej i klasy pochodnej / bazowej są nieco inne. Po przeniesieniu danych do rejestrów lub pamięci RAM (instrukcja mov), wywołaniu metody zapieczętowanej, wykonaj porównanie między dword ptr [ecx], ecx (instrukcja cmp), a następnie wywołaj metodę, podczas gdy klasa pochodna / bazowa wykonuje bezpośrednio metodę. .
Zgodnie z raportem napisanym przez Torbjundorna Granlunda, Opóźnienia instrukcji i przepustowość dla procesorów AMD i Intel x86 , prędkość następujących instrukcji w Intel Pentium 4 wynosi:
Link : https://gmplib.org/~tege/x86-timing.pdf
Oznacza to, że w idealnym przypadku czas potrzebny do wywołania metody zapieczętowanej to 2 cykle, podczas gdy czas potrzebny do wywołania metody klasy pochodnej lub bazowej to 3 cykle.
Optymalizacja kompilatorów spowodowała, że wydajność klas zamkniętych i niezamkniętych jest tak niska, że mówimy o kręgach procesorów iz tego powodu są one nieistotne dla większości zastosowań.
źródło
Uruchom ten kod, a zobaczysz, że zapieczętowane klasy są 2 razy szybsze:
wyjście: Klasa zapieczętowana: 00: 00: 00.1897568 Klasa niezamknięta: 00: 00: 00.3826678
źródło