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ć:)
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
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:
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 jakosealed
. 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.
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:
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: -)
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.)
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.
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].
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".
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 InfrastructureCommon 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.
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.
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
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