Czy zajęcia zamknięte naprawdę oferują korzyści z wydajności?

Natknąłem się na wiele wskazówek optymalizacyjnych, które mówią, że powinieneś oznaczyć swoje klasy 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ś nie tak? Czy brakuje mi przypadku, w którym klasy zamknięte dadzą lepsze wyniki?

Czy ktoś robił testy i widział różnicę?

Pomóż mi się uczyć:)

Author: Lance Roberts, 2008-08-05

11 answers

JITter czasami używa Nie-wirtualnych wywołań metod w klasach zamkniętych, ponieważ nie ma możliwości ich dalszego rozszerzania.

Istnieją złożone zasady dotyczące wywołania typu, Wirtualnego / niewirtualnego, i nie znam ich wszystkich, więc nie mogę ich opisać, ale jeśli poszukasz w google zamkniętych klas i metod wirtualnych, możesz znaleźć kilka artykułów na ten temat.

Zauważ, że każdy rodzaj korzyści wydajnościowych, które uzyskasz z tego poziomu optymalizacji, powinien być traktowany jako ostateczność, zawsze Optymalizuj na poziomie algorytmicznym, zanim zoptymalizujesz na poziomie kodu.

Oto jeden link: bełkotanie na zapieczętowanym słowie kluczowym

 50
Author: Lasse Vågsæther Karlsen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-08-05 12:37:05

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

Problem sprowadza się do call vs callvirt IL kody op. {[2] } jest szybszy niż callvirt, A callvirt jest używany głównie, gdy nie wiesz, czy obiekt został podklasowany. Więc ludzie zakładają, że jeśli zapieczętujesz klasę, wszystkie kody op zmienią się z calvirts na calls i będą szybsze.

Niestety callvirt robi również inne rzeczy, które czynią go użytecznym, jak sprawdzanie referencji null. Oznacza to, że nawet jeśli klasa jest zapieczętowana, Referencja może być nadal null i dlatego potrzebna jest callvirt. Można to obejść (bez konieczności uszczelniania klasy), ale staje się to trochę bezcelowe.

Struktury używają call, ponieważ nie mogą być podklasowane i nigdy nie są null.

Zobacz to pytanie, aby uzyskać więcej informacji:

Call and callvirt

 135
Author: Cameron MacFarland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:14

Jak wiem, nie ma gwarancji korzyści z wykonania. Ale istnieje szansa na zmniejszenie kary za wydajność pod pewnymi szczególnymi warunkami {[5] } metodą sealed. (Klasa sealed sprawia, że wszystkie metody mają być zapieczętowane.)

Ale to zależy od implementacji i środowiska wykonawczego kompilatora.


Szczegóły

[[3]} wiele nowoczesnych procesorów używa długiej struktury rurociągu w celu zwiększenia wydajności. Ponieważ procesor jest niewiarygodnie szybszy od pamięci, procesor musi wstępnie ustawiać kod z pamięci aby przyspieszyć rurociąg. Jeśli kod nie jest gotowy we właściwym czasie, potoki będą bezczynne.

Istnieje duża przeszkoda zwana dynamiczna wysyłka co zakłóca tę "wstępną" optymalizację. Możesz to zrozumieć jako tylko warunkowe rozgałęzienie.

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

PROCESOR nie może wstępnie ustawić następnego kodu do wykonania w tym przypadku, ponieważ następna pozycja kodu jest nieznana, dopóki warunek nie zostanie rozwiązany. Więc to sprawia, że hazard powoduje bezczynność rurociągu. I osiągi kara przez bezczynność jest ogromna w regularnych.

Podobne rzeczy zdarzają się w przypadku nadpisywania metod. Kompilator może określić właściwe nadpisanie metody dla bieżącego wywołania metody, ale czasami jest to niemożliwe. W tym przypadku właściwa metoda może być określona tylko w czasie wykonywania. Jest to również przypadek wysyłania dynamicznego, a głównym powodem języków dynamicznie typowanych są na ogół wolniejsze niż języki statycznie typowane.

Niektóre procesory (w tym najnowsze układy Intela x86) wykorzystują technikę zwaną egzekucja spekulacyjna aby wykorzystać rurociąg nawet w sytuacji. Wystarczy wstępnie ustawić jedną ze ścieżek egzekucji. Ale stopa hit tej techniki nie jest tak wysoka. A niepowodzenie spekulacji powoduje wstrzymanie rurociągu, co również powoduje ogromne kary za wydajność. (jest to całkowicie przez implementację CPU. niektóre mobilne procesory są znane jako nie tego rodzaju optymalizacja w celu oszczędzania energii)

C# jest językiem skompilowanym statycznie. Ale nie zawsze. Nie znam dokładnego stanu i to jest całkowicie zależna od implementacji kompilatora. Niektóre Kompilatory mogą wyeliminować możliwość dynamicznego wysyłania, zapobiegając nadpisywaniu metod, jeśli metoda jest oznaczona jako sealed. Głupie Kompilatory nie mogą. Jest to korzyść wydajnościowa sealed.

This answer (dlaczego jest szybsze przetwarzanie posortowanej tablicy niż niesortowanej tablicy?) znacznie lepiej opisuje przewidywanie gałęzi.

 21
Author: Eonil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:18

Aktualizacja: począwszy od. NET Core 2.0 i. NET Desktop 4.7.1, CLR obsługuje teraz devirtualization. Może przyjmować metody w klasach zamkniętych i zastępować wywołania wirtualne wywołaniami bezpośrednimi - i może to zrobić również dla klas nie zamkniętych, jeśli może to zrobić bezpiecznie.

W takim przypadku (Klasa szczelna, której CLR nie mógł inaczej wykryć jako bezpieczna do dewirtualizacji), Klasa szczelna powinna faktycznie oferować pewien rodzaj korzyści wydajnościowych.

To powiedziane, nie sądzę, że to będzie warto się martwić o , chyba że już sprofilowałeś kod i stwierdziłeś, że jesteś w szczególnie gorącej ścieżce, która jest wywoływana miliony razy, czy coś w tym stylu:

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 dekompilowałem go za pomocą reflektora, aby zobaczyć, jaki był Kod MSIL / align = "left" /

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 Release build) emituje identyczne MSIL, co wygląda następująco:

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 mówią, że sealed zapewnia korzyści wydajnościowe, jest to, że kompilator wie, że klasa nie jest nadpisana, a zatem może używać call zamiast callvirt, ponieważ nie musi sprawdzać, czy nie ma wirtuali itp. Jak udowodniono powyżej, nie jest to prawdą.

Moja następna myśl była taka, że chociaż MSIL jest identyczny, może kompilator JIT inaczej traktuje klasy zamknięte?

Uruchomiłem release build pod debuggerem visual studio i oglądałem dekompilowane 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ć inne). 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 być może uruchomienie pod debuggerem powoduje, że wykonuje on mniej agresywną optymalizację?

Następnie uruchomiłem samodzielny plik wykonywalny release build poza wszelkimi środowiskami debugowania i użył WinDBG + SOS, aby włamać się po zakończeniu programu i zobaczyć niezgodność skompilowanego kodu x86 JIT.

Jak widać z poniższego kodu, podczas pracy poza debuggerem kompilator JIT jest bardziej agresywny i ma wbudowaną metodę WriteIt bezpośrednio do wywołującego. Najważniejsze jest jednak to, że była identyczna podczas wywoływania klasy sealed vs non-sealed. Nie ma żadnej różnicy między zapieczętowanym a nie zapieczętowanym klasy.

Oto, kiedy wywołujemy normalną klasę:

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 zamknięta:

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ć jakaś poprawa wydajności pomiędzy wywołaniem metod na klasach sealed vs non-sealed... Myślę, że teraz jestem szczęśliwa: -)

 19
Author: Orion Edwards,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-14 23:11:31

Oznaczenie klasy sealed nie powinno mieć wpływu na wydajność.

Są przypadki, w których csc może być konieczne wysłanie kodu opcode callvirt zamiast call. Wydaje się jednak, że przypadki te są rzadkie.

I wydaje mi się, że JIT powinien być w stanie emitować takie samo nie-wirtualne wywołanie funkcji dla callvirt, Jak dla call, jeśli wie, że klasa nie ma żadnych podklas (jeszcze). Jeśli istnieje tylko jedna implementacja metody, nie ma sensu wczytywać jej adresu z vtable - wystarczy zadzwonić bezpośrednio do jednej implementacji. W tym przypadku JIT może nawet wbudować funkcję.

Jest to trochę ryzykowne ze strony JIT, ponieważ jeśli podklasa jest później załadowana, JIT będzie musiał wyrzucić ten kod maszynowy i skompilować kod ponownie, emitując prawdziwe wirtualne połączenie. Zgaduję, że nie zdarza się to często w praktyce.

(i tak, projektanci maszyn wirtualnych naprawdę agresywnie dążą do tych małych wygranych wydajności.)

 4
Author: Jason Orendorff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-20 19:16:32

Klasy zamknięte powinny zapewnić poprawę wydajności. Ponieważ Klasa zapieczętowana nie może być wyprowadzona, wszelkie wirtualne członkowie mogą być przekształcane w nie - wirtualne członkowie.

Oczywiście mówimy o naprawdę małych zyskach. Nie oznaczałbym klasy jako zapieczętowane tylko po to, aby uzyskać poprawę wydajności, chyba że profilowanie ujawniło, że jest to problem.

 3
Author: Karl Seguin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-08-05 12:37:07

I nienawidzą klas zamkniętych. Nawet jeśli korzyści wydajnościowe są zdumiewające (w co wątpię), niszcząmodel obiektowy, zapobiegając ponownemu użyciu poprzez dziedziczenie. Na przykład Klasa wątku jest zamknięta. Chociaż widzę, że ktoś może chcieć, aby wątki były tak wydajne, jak to możliwe, mogę również wyobrazić sobie scenariusze, w których możliwość podklasowania wątku miałoby wielkie korzyści. Autorów klas, jeśli Musisz zapieczętować swoje zajęcia dla "wydajność" powody, proszę podać interfejs przynajmniej tak, abyśmy nie musieli owijać i wymieniać wszędzie, gdzie potrzebujemy funkcji, o której zapomniałeś.

Przykład: SafeThread musiał zawinąć klasę Thread, ponieważ Thread jest zapieczętowany i nie ma interfejsu IThread; SafeThread automatycznie zatrzymuje nieobsługiwane wyjątki na wątkach, czego zupełnie brakuje w klasie Thread. [i nie, zdarzenia WYJĄTKÓW nieobsługiwanych powodują, że NIE odbierają wyjątki nieobsługiwane w wątkach drugorzędnych].

 3
Author: Steven A. Lowe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-14 20:15:12

Uważam klasy" sealed "za normalny przypadek i zawsze mam powód, aby pominąć słowo kluczowe "sealed".

Najważniejsze dla mnie powody to:

A) lepsza kontrola czasu kompilacji (uruchamianie interfejsów Nie zaimplementowanych zostanie wykryte podczas kompilacji, a nie tylko podczas wykonywania)

I, główny powód:

B) nadużywanie moich zajęć nie jest możliwe w ten sposób

Szkoda, że Microsoft nie uczynił standardu" zapieczętowanym", A Nie "Nie zapieczętowanym".

 3
Author: Turing Complete,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-03 14:27:15

@Vaibhav, jakie testy wykonałeś by mierzyć wydajność?

Myślę, że trzeba by użyć wirnika i wiercić w CLI i zrozumieć, w jaki sposób uszczelniona Klasa poprawi wydajność.

SSCLI (Rotor)
SSCLI: Shared Source Common Language Infrastructure

Common Language Infrastructure (CLI) jest standardem ECMA, który opisuje rdzeń. NET Ramy. Współdzielone źródło CLI (SSCLI), znany również jako Rotor, jest skompresowane archiwum kodu źródłowego do pracującej realizacji ECMA CLI i język ECMA C# Specyfikacja, technologie w serce Microsoftu. NET Architektura.

 2
Author: hitec,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-08-05 12:40:10

Zamknięte klasy będą co najmniej odrobinę szybsze, ale czasami mogą być szybsze... jeśli JIT Optimizer może inline połączenia, które w przeciwnym razie byłyby wirtualne połączenia. Tak więc, tam, gdzie są często nazywane metody, które są wystarczająco małe, aby być inlined, zdecydowanie rozważyć uszczelnienie klasy.

Jednak najlepszym powodem do zapieczętowania klasy jest stwierdzenie :" nie zaprojektowałem tego, aby było dziedziczone, więc nie pozwolę ci się spalić zakładając, że tak zostało zaprojektowane i nie zamierzam / align = "left" / "

Wiem, że niektórzy mówią, że nienawidzą klas zamkniętych, ponieważ chcą mieć możliwość czerpania z czegokolwiek... ale to często nie jest najbardziej możliwy do utrzymania wybór... ponieważ wystawianie klasy na derywację blokuje Cię o wiele bardziej niż nie wystawianie tego wszystkiego. To podobne do powiedzenia "nienawidzę klas, które mają prywatnych członków... Często nie mogę zmusić klasy do zrobienia tego, co chcę, bo nie mam dostęp."Hermetyzacja jest ważna... uszczelnienie jest jedną z form hermetyzacji.

 2
Author: Brian Kennedy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-01 10:37:28

Uruchom ten kod, a zobaczysz, że zamknięte 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 szczelności: 00: 00: 00.1897568 Klasa nonealed : 00:00:00.3826678

 -9
Author: RuslanG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-26 17:58:10