Fare sigillato classi di offrire Vantaggi di prestazioni?
-
08-06-2019 - |
Domanda
Mi sono imbattuto in un sacco di suggerimenti per l'ottimizzazione che dicono che si dovrebbe segnare classi come sigillato per ottenere ulteriori vantaggi di prestazioni.
Ho eseguito alcuni test per verificare le prestazioni e differenziale trovato nessuno.Sto facendo qualcosa di sbagliato?Mi manca il caso in cui le classi sealed darà risultati migliori?
Qualcuno ha l'esecuzione di test e visto la differenza?
Mi aiutano a imparare :)
Soluzione
Il JITter a volte uso non virtuale chiamate ai metodi nelle classi chiuse, poiché non c'è modo che può essere ulteriormente estesa.
Ci sono regole complesse per quanto riguarda la chiamata di tipo virtuale/nonvirtual, e io non li conosco tutti, quindi non posso davvero contorno per voi, ma se cercate su google per classi chiuse e i metodi virtuali, si potrebbe trovare un po ' di articoli sull'argomento.
Si noti che qualsiasi tipo di performance benefici che si possono ottenere da questo livello di ottimizzazione dovrebbe essere considerata come ultima risorsa, sempre di ottimizzare il livello algoritmico prima di ottimizzare il livello di codice.
Ecco un link che citano questo: Divagando la parola chiave sealed
Altri suggerimenti
La risposta è no, sigillato classi di non eseguire meglio di quanto non sigillati.
Il problema si riduce alla call
vs callvirt
IL op codici. Call
è più veloce callvirt
, e callvirt
è utilizzato principalmente quando non sai se l'oggetto è stato sottoclasse.Così la gente considera che, se tenuta con una classe op codici cambia da calvirts
per calls
e sarà più veloce.
Purtroppo callvirt
fa altre cose che la rendono utile anche, come il controllo per un riferimento null.Questo significa che anche se una classe è sigillato, il riferimento potrebbe ancora essere null e quindi un callvirt
è necessaria.È possibile ottenere intorno a questo (senza bisogno di sigillare la classe), ma diventa un po ' inutile.
Utilizzare le strutture call
perché non può essere una sottoclasse e non sono mai null.
Vedere questa domanda per ulteriori informazioni:
Aggiornamento:Come di .NET Core 2.0 e .NET Desktop 4.7.1, CLR ora supporta devirtualization.Si può prendere metodi di classi chiuse e sostituire virtuale chiamate con chiamate dirette, e può fare anche questo, per non sigillati classi se si può capire che è sicuro farlo.
In tal caso (una classe chiusa CLR diversamente non rilevare come sicuri devirtualise), una classe chiusa in realtà dovrebbe offrire un po ' di vantaggio in termini di prestazioni.
Detto questo, non credo sarebbe vale la pena preoccuparsi a meno che non si era già profilato il codice e determinato che si erano particolarmente calda percorso detta milioni di volte, o qualcosa di simile:
Originale Risposta:
Ho fatto il seguente programma di prova, e quindi decompilato utilizzando Riflettore per vedere che cosa il codice MSIL è stata emessa.
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");
}
In tutti i casi, il compilatore C# (Visual studio 2010 in build di Rilascio di configurazione) emette identici MSIL, che è come segue:
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
Il citato spesso la ragione che la gente dice sigillato fornisce prestazioni di vantaggi è che il compilatore sa che la classe non viene sovrascritto, e quindi non è possibile utilizzare call
invece di callvirt
non devo controllare virtuali, etc.Come dimostrato sopra, questo non è vero.
Il mio pensiero successivo è stato che, anche se il codice MSIL è identico, forse il compilatore JIT tratta classi chiuse in modo diverso?
Mi sono imbattuto in una build di rilascio sotto il debugger di visual studio e visto decompilato x86 uscita.In entrambi i casi, il codice x86 è stato identico, con l'eccezione dei nomi di classe e funzione di indirizzi di memoria (che ovviamente deve essere diverso).Qui è
// 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
Allora ho pensato che forse corre sotto il debugger cause di svolgere in modo meno aggressivo di ottimizzazione?
Ho quindi eseguito un autonomo build di rilascio eseguibile al di fuori di qualsiasi ambienti di debug, e utilizzato WinDBG + SOS interruzione dopo che il programma ha completato, e vista la dissasembly del JIT compilato il codice x86.
Come si può vedere dal codice riportato di seguito, quando si esegue fuori il debugger JIT compiler è più aggressivo, e ha inline il WriteIt
metodo dritto in chiamante.La cosa fondamentale però è che era identico quando si chiama una sigillato vs non-classe chiusa.Non c'è nessuna differenza tra una sigillato o nonsealed classe.
Qui è quando si chiama una classe normale:
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 una classe chiusa:
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
Per me, questo fornisce la prova solida che c' non essere qualsiasi miglioramento di prestazioni tra la chiamata di metodi sigillato vs non-classi chiuse...Penso che ora sono felice :-)
Che io sappia, non vi è alcuna garanzia di prestazioni.Ma c'è una possibilità per ridurre le prestazioni sotto alcune condizioni specifiche sigillate con metodo.(classe chiusa rende tutti i metodi per essere sigillato.)
Ma è per implementazione del compilatore e ambiente di esecuzione.
Dettagli
Molte delle moderne Cpu lunghe pipeline struttura per aumentare le prestazioni.Perché la CPU è incredibilmente più veloce di memoria, CPU ha prelettura codice dalla memoria per accelerare la pipeline.Se il codice non è pronto al momento giusto, le condutture di inattività.
C'è un grande ostacolo chiamato dynamic dispatch che sconvolge questo 'prelettura' ottimizzazione.Si può capire questo come un condizionale.
// 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();
CPU non è in grado di prelettura successiva esecuzione di codice in questo caso, perché il successivo codice posizione è sconosciuta fino a quando la condizione è risolto.Quindi, questo rende pericolo cause pipeline di inattività.E delle prestazioni da idle è enorme regolari.
Una cosa simile succede in caso di metodo di override.Compilatore può determinare un corretto metodo di override per la corrente di chiamata di metodo, ma a volte è impossibile.In questo caso, il corretto metodo può essere determinato solo in fase di runtime.Questo è anche un caso di invio dinamico e, una ragione principale del dinamicamente tipizzato lingue sono generalmente più lenti di staticamente tipizzato lingue.
Alcune CPU (compresi i recenti processori Intel x86 chips) utilizza la tecnica chiamata esecuzione speculativa per utilizzare la pipeline anche sulla situazione.Solo prefetch uno di percorso di esecuzione.Ma il tasso di successo di questa tecnica non è così alto.E la speculazione a causa di un errore pipeline di stallo che rende anche una enorme pena di prestazioni.(questo è completamente da CPU attuazione.alcune CPU mobile è noto come non questo tipo di ottimizzazione per risparmiare energia)
Fondamentalmente, C# è un linguaggio compilato staticamente.Ma non sempre.Non so condizione esatta e questo è interamente a implementazione del compilatore.Alcuni compilatori può eliminare la possibilità di invio dinamico, impedendo l'override dei metodi se il metodo è contrassegnato come sealed
.Stupido compilatori potrebbe non.Questo è il beneficio di prestazioni di sealed
.
Questa rispostaPerché è più veloce per elaborare un array ordinato di una lista non ordinata?) descrive la branch prediction molto meglio.
La marcatura di una classe sealed
dovrebbe avere alcun impatto sulle prestazioni.
Ci sono casi in cui csc
potrebbe emettere un callvirt
opcode invece di un call
opcode.Tuttavia, sembra che quelli sono casi rari.
E mi sembra che il JIT dovrebbe essere in grado di emettere la stessa non-virtuale chiamata di funzione per callvirt
che sarebbe per call
, se si sa che la classe non ha sottoclassi (ancora).Se solo una implementazione del metodo esiste, non c'è nessun punto di carico suo indirizzo da un vtable, chiamate l'una implementazione direttamente.Per quella materia, il JIT può anche inline funzione.
E 'un po' un azzardo JIT parte, perché se una sottoclasse è successivamente caricato, il JIT dovranno gettare via che il codice macchina e compilare di nuovo il codice, con l'emissione di una vera chiamata virtuale.La mia ipotesi è che questo non accade spesso, nella pratica.
(E sì, VM designer davvero fare aggressivamente perseguire queste piccole prestazioni wins).
Classi chiuse dovrebbe fornire un miglioramento delle prestazioni.Dal momento che una classe chiusa non può essere derivata, eventuali membri virtuali può essere trasformato in non-membri virtuali.
Naturalmente, stiamo parlando davvero di piccoli guadagni.Io non segno di una classe sealed solo per ottenere un miglioramento delle prestazioni, a meno che il profiling ha rivelato di essere un problema.
<off-topic-rant>
Io odiano classi chiuse.Anche se i vantaggi di prestazioni sono sorprendenti (che dubito), si distruggere il modello object oriented, impedendo il riutilizzo tramite ereditarietà.Per esempio, la classe Thread è sigillato.Mentre vedo che si potrebbe desiderare di thread per essere il più efficiente possibile, posso anche immaginare scenari in cui essere in grado di creare una sottoclasse di Thread avrebbe grandi benefici.Classe autori, se si deve sigillare le classi per la "performance" motivi si prega di fornire un'interfaccia almeno così non dobbiamo avvolgere e sostituire ovunque che abbiamo bisogno di una funzione che si è dimenticato.
Esempio: SafeThread aveva avvolgere il Filo di classe perché il Thread è sigillato e non c'è IThread interfaccia;SafeThread automaticamente trappole le eccezioni non gestite sul thread, qualcosa di completamente mancanti dalla classe Thread.[e no, l'eccezione non gestita eventi non raccogliere le eccezioni non gestite nel thread secondari].
</off-topic-rant>
Io considero "sigillato" classi caso normale e ho SEMPRE un motivo per omettere il "sigillato" parola chiave.
Le ragioni più importanti per me sono:
a) compilare i controlli orari (colata di interfacce non implementato saranno rilevati in fase di compilazione, non solo in fase di esecuzione)
e, motivo superiore:
b) l'Abuso dei miei corsi non è possibile,
Vorrei Microsoft avrebbe fatto "sigillato" standard "(e non "aperti".
@Vaibhav, che tipo di test hai eseguito per misurare le prestazioni?
Credo che uno dovrebbe utilizzare Rotore e forare il CLI e capire come una classe chiusa potrebbe migliorare le prestazioni.
SSCLI (Rotore)
SSCLI:Shared Source Common Language InfrastructureIl Common Language Infrastructure (CLI) è lo standard ECMA descrive il nucleo .NET Quadro.La Sorgente comune CLI (SSCLI), noto anche come il Rotore, è un archivio compresso del codice sorgente per un lavoro di implementazione del ECMA CLI e ECMA linguaggio C# specifiche, tecnologie all' cuore di Microsoft .NET architettura.
sigillate le lezioni saranno almeno un po ' più veloce, ma a volte può essere waayyy più veloce...se il JIT Optimizer può inline chiamate che sarebbe stato altrimenti virtuale chiamate.Così, dove c'è spesso chiamato metodi che sono abbastanza piccolo per essere sostituite, sicuramente tenuta in considerazione la classe.
Tuttavia, la ragione migliore per sigillare una classe è quello di dire "non ho progettato questo per essere ereditata, quindi non ho intenzione di farvi ottenere bruciato dal supponendo che è stato progettato per essere così, e non ho intenzione di bruciare me stesso di ottenere bloccato in una fase di implementazione, poiché mi consente di derivare da esso."
So che alcuni qui hanno detto che odio classi chiuse perché vogliono la possibilità di derivare dal nulla...ma che, SPESSO, non è più gestibile scelta...perché l'esposizione di una classe di derivazione ti vincola molto di più di quanto non esponendo tutti che.Il suo simile a dire "mi fanno schifo le classi che hanno privato i membri...Spesso non riesco a fare la classe di fare quello che voglio perché non ho accesso." L'incapsulamento è importante...la sigillatura è una forma di incapsulamento.
Eseguire questo codice e vedrai che le classi sealed sono 2 volte più veloce:
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";
}
}
output:Classe chiusa :00:00:00.1897568 NonSealed classe :00:00:00.3826678