Czy klasy zamknięte naprawdę zapewniają korzyści w zakresie wydajności?


140

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ć :)


9
Nie sądzę, aby zajęcia zamknięte miały na celu zwiększenie wydajności. Fakt, że tak się dzieje, może być przypadkowy. Oprócz tego profiluj swoją aplikację po jej refaktoryzacji, aby używać zapieczętowanych klas i określ, czy była warta wysiłku. Blokowanie rozszerzalności w celu wykonania niepotrzebnej mikrooptymalizacji będzie Cię kosztować na dłuższą metę. Oczywiście, jeśli sprofilowałeś i pozwala ci to osiągnąć twoje wzorce wydajności (zamiast perf dla dobra perfekcji), wtedy możesz podjąć decyzję jako zespół, jeśli jest to warte wydanych pieniędzy. Jeśli zapieczętowałeś zajęcia z powodów niedoskonałych, zatrzymaj je :)
Merlyn Morgan-Graham

1
Czy próbowałeś z refleksją? Czytałem gdzieś, że tworzenie instancji przez odbicie jest szybsze w klasach zapieczętowanych
dniu

O ile wiem, nie ma. Sealed istnieje z innego powodu - aby zablokować rozszerzalność, co może być przydatne / potrzebne w wielu przypadkach. Optymalizacja wydajności nie była tutaj celem.
TomTom

... ale jeśli myślisz o kompilatorze: jeśli twoja klasa jest zapieczętowana, znasz adres metody, którą wywołujesz w swojej klasie w czasie kompilacji. Jeśli twoja klasa nie jest zapieczętowana, musisz rozwiązać metodę w czasie wykonywania, ponieważ może być konieczne wywołanie zastąpienia. Z pewnością będzie nieistotny, ale widziałem, że będzie jakaś różnica.
Dan Puzey

Tak, ale tego rodzaju nie przekłada się na realne korzyści, jak wskazano w PO. Różnice architektoniczne są / mogą być dużo bardziej istotne.
TomTom

Odpowiedzi:


60

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


2
link „wędrowanie” jest interesujący, ponieważ brzmi jak dobro techniczne, ale w rzeczywistości jest nonsensem. Przeczytaj komentarze do artykułu, aby uzyskać więcej informacji. Podsumowanie: 3 podane powody to Wersjonowanie, Wydajność i Bezpieczeństwo / Przewidywalność - [patrz następny komentarz]
Steven A. Lowe

1
[ciąg dalszy] Wersjonowanie ma zastosowanie tylko wtedy, gdy nie ma podklas, tak, ale rozszerz ten argument na każdą klasę i nagle nie masz dziedziczenia i zgadnij co, język nie jest już zorientowany obiektowo (ale tylko oparty na obiektach)! [patrz dalej]
Steven A. Lowe

3
[ciąg dalszy] przykład wydajności to żart: optymalizacja wywołania metody wirtualnej; dlaczego klasa zapieczętowana miałaby przede wszystkim metodę wirtualną, skoro nie można jej podklasy? Wreszcie argument dotyczący bezpieczeństwa / przewidywalności jest ewidentnie głupi: „nie możesz go używać, więc jest bezpieczny / przewidywalny”. LOL!
Steven A. Lowe

16
@Steven A. Lowe - Myślę, że to, co Jeffrey Richter próbował powiedzieć w nieco okrężny sposób, to to, że jeśli zostawisz swoją klasę bez zapieczętowania, musisz pomyśleć o tym, jak klasy pochodne mogą / będą jej używać, a jeśli nie masz czasu lub chęci, aby zrobić to prawidłowo, a następnie zapieczętować to, ponieważ w przyszłości jest mniej prawdopodobne, że spowoduje to przełomowe zmiany w kodzie innych osób. To wcale nie jest nonsens, to zdrowy rozsądek.
Greg Beech

6
Klasa zapieczętowana może mieć metodę wirtualną, ponieważ może pochodzić z klasy, która ją deklaruje. Kiedy później zadeklarujesz zmienną zapieczętowanej klasy potomnej i wywołasz tę metodę, kompilator może wyemitować bezpośrednie wywołanie znanej implementacji, ponieważ wie, że nie ma możliwości, aby była inna niż to, co jest w znanej - at-compile-time vtable dla tej klasy. Jeśli chodzi o zapieczętowane / niezapieczętowane, to inna dyskusja i zgadzam się z powodami, dla których klasy są domyślnie zapieczętowane.
Lasse V. Karlsen

143

Odpowiedź brzmi: nie, klasy zamknięte nie działają lepiej niż nie zamknięte.

Problem sprowadza się do kodów operacyjnych callvs callvirtIL. Calljest szybszy niż callvirti callvirtjest 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ę z calvirtsnacalls i będą szybsze.

Niestety callvirtrobi 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ń


5
AFAIK, okoliczności, w których calljest używany, to: w sytuacji new T().Method(), dla structmetod, dla niewirtualnych wywołań virtualmetod (takich jak base.Virtual()) lub dla staticmetod. Wszędzie indziej używa callvirt.
porusza się

1
Uhh ... zdaję sobie sprawę, że to jest stare, ale to nie jest do końca w porządku ... wielką wygraną w przypadku klas zamkniętych jest to, że JIT Optimizer może wbudować połączenie ... w takim przypadku klasa zamknięta może być ogromną wygraną .
Brian Kennedy

5
Dlaczego ta odpowiedź jest błędna. Z dziennika zmian Mono: „Optymalizacja dewirtualizacji dla zapieczętowanych klas i metod, poprawiająca wydajność pystone IronPython 2.0 o 4%. Inne programy mogą oczekiwać podobnej poprawy [Rodrigo].”. Klasy zamknięte mogą poprawić wydajność, ale jak zwykle zależy to od sytuacji.
Smilediver

1
@Smilediver Może to poprawić wydajność, ale tylko wtedy, gdy masz zły JIT (nie mam pojęcia, jak dobre są obecnie JIT .NET - był pod tym względem dość zły). Na przykład Hotspot będzie wbudowywał wywołania wirtualne i dezoptymalizował później, jeśli zajdzie taka potrzeba - dlatego płacisz dodatkowy narzut tylko wtedy, gdy faktycznie podklasujesz klasę (a nawet wtedy niekoniecznie).
Voo,

1
-1 JIT niekoniecznie musi generować ten sam kod maszynowy dla tych samych rozkazów IL. Sprawdzanie wartości null i wirtualne wywołanie są ortogonalnymi i oddzielnymi krokami funkcji callvirt. W przypadku typów zapieczętowanych kompilator JIT może nadal optymalizować część callvirt. To samo dzieje się, gdy kompilator JIT może zagwarantować, że odwołanie nie będzie zerowe.
prawej

29

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.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

We wszystkich przypadkach kompilator C # (Visual Studio 2010 w konfiguracji kompilacji wydania) emituje identyczny plik MSIL, który jest następujący:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

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ć callzamiastcallvirt 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

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

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ł WriteItmetodę 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:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

Vs klasa szczelna:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

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 :-)


Czy jest jakiś powód, dla którego opublikowałeś tę odpowiedź dwa razy, zamiast zamykać drugą jako duplikat? Wyglądają jak ważne duplikaty, chociaż to nie moja dziedzina.
Flexo

1
Co by było, gdyby metody były dłuższe (więcej niż 32 bajty kodu IL), aby uniknąć wstawiania i zobaczyć, jaka operacja wywołania zostanie użyta? Jeśli jest wbudowany, nie widzisz wywołania, więc nie możesz ocenić efektów.
ygoe

Jestem zdezorientowany: dlaczego tak jest, callvirtskoro te metody nie są wirtualne?
dziwaczny

@freakish Nie pamiętam, gdzie to widziałem, ale przeczytałem, że CLR używany callvirtdo 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ąć callvirti 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ę)
Orion Edwards

1
To dowód na to, że podjęto wysiłek zgłębienia problemu do poziomu kodu maszynowego, ale bądź bardzo ostrożny, składając stwierdzenia typu „to stanowi solidny dowód na to, że nie ma żadnej poprawy wydajności”. Pokazałeś, że w jednym konkretnym scenariuszu nie ma różnicy w wyjściach natywnych. To jeden punkt danych i nie można zakładać, że uogólnia się on we wszystkich scenariuszach. Na początek Twoje klasy nie definiują żadnych metod wirtualnych, więc wirtualne wywołania nie są w ogóle wymagane.
Matt Craig,

24

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.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

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 programu sealed.


Ta odpowiedź ( Dlaczego posortowana tablica jest szybsza niż nieposortowana? ) Znacznie lepiej opisuje prognozowanie rozgałęzienia.


1
Procesory klasy Pentium bezpośrednio pobierają z wyprzedzeniem pośrednią wysyłkę. Czasami przekierowanie wskaźnika funkcji jest szybsze niż (niemożliwe) z tego powodu.
Joshua

2
Jedną z zalet funkcji niewirtualnych lub zapieczętowanych jest to, że można je wstawiać w wielu sytuacjach.
CodesInChaos

4

<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>


35
Nie pieczętuję zajęć ze względu na wydajność. Uszczelniam je ze względów konstrukcyjnych. Projektowanie pod kątem dziedziczenia jest trudne, a ten wysiłek zostanie zmarnowany przez większość czasu. Całkowicie się zgadzam co do dostarczania interfejsów - to znacznie lepsze rozwiązanie niż rozszczelnianie klas.
Jon Skeet

6
Hermetyzacja jest ogólnie lepszym rozwiązaniem niż dziedziczenie. Aby wziąć konkretny przykład wątku, wychwytywanie wyjątków wątku łamie zasadę zastępowania Liskova, ponieważ zmieniłeś udokumentowane zachowanie klasy Thread, więc nawet gdybyś mógł z niego wyprowadzić, nie byłoby rozsądne twierdzenie, że możesz używać SafeThread wszędzie możesz użyć Thread. W takim przypadku lepiej byłoby zamknąć Thread w innej klasie, która ma inne udokumentowane zachowanie, co możesz zrobić. Czasami rzeczy są zapieczętowane dla twojego własnego dobra.
Greg Beech

1
@ [Greg Beech]: opinia, nie fakt - możliwość odziedziczenia po Thread, aby naprawić ohydny błąd w jego projekcie, NIE jest złą rzeczą ;-) I myślę, że przesadzasz z LSP - możliwą do udowodnienia właściwość q (x) w ten przypadek to „nieobsługiwany wyjątek niszczy program”, który nie jest „pożądaną właściwością” :-)
Steven A. Lowe

1
Nie, ale miałem swoją część kiepskiego kodu, w którym umożliwiłem innym programistom nadużywanie moich rzeczy, nie zamykając ich lub zezwalając na narożne przypadki. Obecnie większość mojego kodu to asercje i inne rzeczy związane z kontraktami. I jestem całkiem otwarty na temat tego, że robię to tylko po to, żeby uprzykrzyć mu się.
Turing Ukończono

2
Ponieważ robimy tutaj nie na temat tyrady, tak jak ty nienawidzisz klas zamkniętych, ja nienawidzę połykania wyjątków. Nie ma nic gorszego niż sytuacja, w której coś się dzieje, ale program trwa. JavaScript jest moim ulubionym. Dokonujesz zmiany w jakimś kodzie i nagle kliknięcie przycisku nie robi absolutnie nic. Wspaniały! ASP.NET i UpdatePanel to kolejna; poważnie, jeśli mój program do obsługi przycisku wyrzuci, to jest to wielka sprawa i musi się WYERZYWAĆ, więc wiem, że jest coś, co wymaga naprawy! Przycisk, który nic nie robi, jest bardziej bezużyteczny niż przycisk, który wyświetla ekran awarii!
Roman Starkov

4

Oznaczanie zajęć sealed powinno mieć wpływu na wydajność.

Istnieją przypadki, w których cscmoże być konieczne wyemitowanie callvirtkodu operacyjnego zamiast callkodu 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).


3

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.


Oni powinni , ale wydaje się, że nie oni. Gdyby CLR miał koncepcję niezerowych typów referencyjnych, to zapieczętowana klasa byłaby naprawdę lepsza, ponieważ kompilator mógłby emitować callzamiast callvirt... Chciałbym również niezerowe typy referencyjne z wielu innych powodów ... westchnienie :-(
Orion Edwards

3

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”.


Uważam, że „pominąć” (pominąć) powinno być „wyemitować” (wyprodukować)?
user2864740

2

@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ść.

SSCLI (wirnik)
SSCLI: wspólna infrastruktura języka źródłowego

Common Language Infrastructure (CLI) to standard ECMA, który opisuje rdzeń platformy .NET Framework. Shared Source CLI (SSCLI), znany również jako Rotor, to skompresowane archiwum kodu źródłowego działającej implementacji interfejsu ECMA CLI i specyfikacji języka ECMA C #, technologii stanowiących podstawę architektury .NET firmy Microsoft.


Test obejmował stworzenie hierarchii klas, z niektórymi metodami, które wykonywały fikcyjną pracę (głównie manipulowanie napisami). Niektóre z tych metod były wirtualne. Wzywali się tu i tam. Następnie wywołując te metody 100, 10000 i 100000 razy ... i mierząc upływający czas. Następnie uruchom je po oznaczeniu klas jako zapieczętowanych. i mierzymy ponownie. Nie ma w nich różnicy.
Vaibhav

2

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.


Kompilator C # będzie nadal używał 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.
Orion Edwards,

1

Aby naprawdę je zobaczyć, musisz przeanalizować kod e skompilowany JIT (ostatni).

Kod C #

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

Kod MIL

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

JIT - skompilowany kod

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

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:

  • mov : ma 1 cykl jako opóźnienie, a procesor może wytrzymać 2,5 instrukcji na cykl tego typu
  • cmp : ma 1 cykl jako opóźnienie, a procesor może wytrzymać 2 instrukcje na cykl tego typu

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ń.


-10

Uruchom ten kod, a zobaczysz, że zapieczętowane klasy są 2 razy szybsze:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

wyjście: Klasa zapieczętowana: 00: 00: 00.1897568 Klasa niezamknięta: 00: 00: 00.3826678


9
Jest z tym kilka problemów. Po pierwsze, nie resetujesz stopera między pierwszym a drugim testem. Po drugie, sposób wywoływania metody oznacza, że ​​wszystkie kody operacyjne będą wywoływane, a nie callvirt, więc typ nie ma znaczenia.
Cameron MacFarland

1
RuslanG, zapomniałeś zadzwonić do watch.Reset () po uruchomieniu pierwszego testu. o_O:]

Tak, powyższy kod zawiera błędy. Z drugiej strony, kto może się chwalić, że nigdy nie wprowadza błędów w kodzie? Ale ta odpowiedź ma jedną ważną rzecz lepszą niż wszystkie inne: próbuje zmierzyć dany efekt. I ta odpowiedź również zawiera kod dla wszystkich, którzy sprawdzają i [masowo] odrzucają (zastanawiam się, dlaczego masowe głosowanie jest konieczne?). W przeciwieństwie do innych odpowiedzi. Moim zdaniem to zasługuje na szacunek ... Proszę również zauważyć, że użytkownik jest nowicjuszem, więc też niezbyt przyjazne podejście. Kilka prostych poprawek i ten kod staje się przydatny dla nas wszystkich. Napraw i udostępnij poprawioną wersję, jeśli się odważysz
Roland Pihlakas,

Nie zgadzam się z @RolandPihlakas. Jak powiedziałeś, „kto może się chwalić, że nigdy nie wprowadza błędów w kodzie”. Odpowiedź -> Nie, żadna. Ale nie o to chodzi. Chodzi o to, że jest to błędna informacja, która może wprowadzić w błąd innego nowego programistę. Wiele osób może łatwo przeoczyć, że stoper nie został zresetowany. Mogliby uwierzyć, że informacje porównawcze są prawdziwe. Czy to nie jest bardziej szkodliwe? Ktoś, kto szybko szuka odpowiedzi, może nawet nie przeczytać tych komentarzy, raczej zobaczy odpowiedź i może w to uwierzyć.
Emran Hussain
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.