Le classi sigillate offrono davvero vantaggi prestazionali?


140

Ho trovato molti suggerimenti per l'ottimizzazione che dicono che dovresti contrassegnare le tue lezioni come sigillate per ottenere benefici extra in termini di prestazioni.

Ho eseguito alcuni test per verificare il differenziale di prestazioni e non ne ho trovato nessuno. Sto facendo qualcosa di sbagliato? Mi manca il caso in cui le classi sigillate daranno risultati migliori?

Qualcuno ha eseguito dei test e visto una differenza?

Aiutami a imparare :)


9
Non credo che le classi sigillate fossero destinate a dare un aumento perfetto. Il fatto che lo facciano potrebbe essere casuale. Inoltre, profila la tua app dopo averla rifattorizzata per utilizzare le classi sigillate e determinare se ne è valsa la pena. Bloccare la tua estensibilità per effettuare una micro-ottimizzazione non necessaria ti costerà a lungo termine. Ovviamente, se hai profilato e ti consente di raggiungere i tuoi benchmark perf (piuttosto che perf per il bene di perf), allora puoi prendere una decisione come squadra se vale la pena spendere i soldi. Se hai lezioni sigillate per motivi non perfetti, tienile :)
Merlyn Morgan-Graham,

1
Hai provato con la riflessione? Ho letto da qualche parte che un'istanza di riflesso è più veloce con le classi sigillate
onof

Per quanto ne sappia, non c'è nessuno. Sigillato esiste per una ragione diversa: bloccare l'estensibilità, che può essere utile / necessario in molti casi. L'ottimizzazione delle prestazioni non era un obiettivo qui.
TomTom,

... ma se pensi al compilatore: se la tua classe è sigillata, conosci l'indirizzo di un metodo che chiami sulla tua classe al momento della compilazione. Se la classe non è sigillata, è necessario risolvere il metodo in fase di esecuzione perché potrebbe essere necessario chiamare una sostituzione. Sarà certamente trascurabile, ma ho potuto vedere che ci sarebbe qualche differenza.
Dan Puzey,

Sì, ma questo tipo di cose non si traduce in benefici reali, come ha sottolineato l'OP. Le differenze architettoniche sono / possono essere molto più rilevanti.
TomTom,

Risposte:


60

A volte JITter utilizzerà chiamate non virtuali a metodi in classi sigillate poiché non è possibile estenderle ulteriormente.

Esistono regole complesse per quanto riguarda il tipo di chiamata, virtuale / non virtuale, e non le conosco tutte, quindi non posso davvero delinearle per te, ma se cerchi Google per classi sigillate e metodi virtuali potresti trovare alcuni articoli sull'argomento.

Si noti che qualsiasi tipo di vantaggio in termini di prestazioni che si otterrebbe da questo livello di ottimizzazione dovrebbe essere considerato come ultima risorsa, ottimizzare sempre a livello algoritmico prima di ottimizzare a livello di codice.

Ecco un link che menziona questo: scherzare sulla parola chiave sigillata


2
il collegamento "sconclusionato" è interessante in quanto sembra una bontà tecnica ma in realtà è una sciocchezza. Leggi i commenti sull'articolo per maggiori informazioni. Riepilogo: i 3 motivi addotti sono Versioning, Performance e Security / Predictability - [vedi commento successivo]
Steven A. Lowe,

1
[continua] Il versioning si applica solo quando non ci sono sottoclassi, duh, ma estendi questo argomento a ogni classe e all'improvviso non hai ereditarietà e indovina un po ', il linguaggio non è più orientato agli oggetti (ma semplicemente basato sugli oggetti)! [vedi il prossimo]
Steven A. Lowe,

3
[continua] l'esempio di prestazione è uno scherzo: ottimizzazione di una chiamata al metodo virtuale; perché una classe sigillata dovrebbe in primo luogo avere un metodo virtuale poiché non può essere sottoclassata? Infine, l'argomento Sicurezza / prevedibilità è chiaramente fatuo: "non puoi usarlo, quindi è sicuro / prevedibile". LOL!
Steven A. Lowe,

16
@Steven A. Lowe - Penso che ciò che Jeffrey Richter stava cercando di dire in modo leggermente indiretto è che se lasci la tua classe non sigillata devi pensare a come le classi derivate possono / lo useranno, e se non hai il tempo o inclinazione a farlo correttamente, quindi sigillalo in quanto è meno probabile che provochi in futuro cambiamenti nel codice altrui. Non è affatto una sciocchezza, è un buon senso comune.
Greg Beech,

6
Una classe sigillata potrebbe avere un metodo virtuale poiché potrebbe derivare da una classe che lo dichiara. Quando poi, in seguito, dichiari una variabile della classe discendente sigillata e chiami quel metodo, il compilatore potrebbe emettere una chiamata diretta all'implementazione nota, poiché sa che non è possibile che ciò sia diverso da ciò che è noto- vtable in fase di compilazione per quella classe. Per quanto riguarda sigillato / non sigillato, questa è una discussione diversa, e sono d'accordo con i motivi per rendere le lezioni sigillate di default.
Lasse V. Karlsen,

143

La risposta è no, le classi sigillate non funzionano meglio di quelle non sigillate.

Il problema riguarda i codici operativi callvs callvirtIL. Callè più veloce di callvirted callvirtè usato principalmente quando non sai se l'oggetto è stato sottoclassato. Così la gente per scontato che se si sigillare una classe tutti i codici op cambieranno da calvirtsad callse sarà più veloce.

Sfortunatamente callvirtfa anche altre cose che lo rendono utile, come la ricerca di riferimenti null. Ciò significa che anche se una classe è sigillata, il riferimento potrebbe essere nullo e quindi callvirtè necessario. Puoi aggirare questo (senza bisogno di sigillare la classe), ma diventa un po 'inutile.

Le strutture usano callperché non possono essere sottoclassate e non sono mai nulle.

Vedi questa domanda per ulteriori informazioni:

Chiama e chiama


5
AFAIK, le circostanze in cui callviene utilizzato sono: nella situazione new T().Method(), per structmetodi, per chiamate non virtuali a virtualmetodi (come base.Virtual()) o per staticmetodi. Ovunque altro usa callvirt.
Porges,

1
Uhh ... mi rendo conto che è vecchio, ma non è del tutto giusto ... la grande vittoria con le classi sigillate è quando l'ottimizzatore JIT può integrare la chiamata ... in quel caso, la classe sigillata può essere una vittoria enorme .
Brian Kennedy,

5
Perché questa risposta è sbagliata. Dal log delle modifiche Mono: "Ottimizzazione della devirtualizzazione per classi e metodi sigillati, migliorando del 4% le prestazioni del pystone di IronPython 2.0. Altri programmi possono aspettarsi un miglioramento simile [Rodrigo].". Le classi sigillate possono migliorare le prestazioni, ma come sempre dipende dalla situazione.
Smilediver,

1
@Smilediver Può migliorare le prestazioni, ma solo se hai una JIT cattiva (non ho idea di quanto siano buone le JIT .NET al giorno d'oggi - una volta era piuttosto cattiva in questo senso). L'hotspot, ad esempio, incorporerà le chiamate virtuali e, in caso di necessità, lo smantellerà in seguito, quindi pagherai l'overhead aggiuntivo solo se effettivamente esegui la sottoclasse della classe (e anche in questo caso non necessariamente).
Voo

1
-1 La JIT non deve necessariamente generare lo stesso codice macchina per gli stessi codici OP IL. Il controllo null e la chiamata virtuale sono fasi ortogonali e separate di callvirt. Nel caso di tipi sigillati, il compilatore JIT può comunque ottimizzare parte di callvirt. Lo stesso vale quando il compilatore JIT può garantire che un riferimento non sarà nullo.
rightfold

29

Aggiornamento: A partire da .NET Core 2.0 e .NET Desktop 4.7.1, CLR ora supporta la devirtualizzazione. Può prendere metodi in classi sigillate e sostituire chiamate virtuali con chiamate dirette - e può farlo anche per classi non sigillate se riesce a capire che è sicuro farlo.

In tal caso (una classe sigillata che il CLR non potrebbe altrimenti rilevare come sicura da devirtualizzare), una classe sigillata dovrebbe effettivamente offrire qualche tipo di vantaggio in termini di prestazioni.

Detto questo, non penserei che valga la pena preoccuparsi se non avessi già profilato il codice e stabilito che ti trovavi in ​​un percorso particolarmente hot chiamato milioni di volte, o qualcosa del genere:

https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/


Risposta originale:

Ho creato il seguente programma di test e poi lo ho decompilato usando Reflector per vedere quale codice MSIL è stato emesso.

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 Configurazione build di rilascio) emette MSIL identico, che è il seguente:

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 motivo spesso citato che la gente dice sigillato offre vantaggi in termini di prestazioni è che il compilatore sa che la classe non è sostituita, e quindi può usare callinvece di callvirtcome non deve controllare per i virtuali, ecc. Come dimostrato sopra, questo non è vero.

Il mio pensiero successivo è stato che, sebbene MSIL sia identico, forse il compilatore JIT tratta le classi sigillate in modo diverso?

Ho eseguito una build di rilascio con il debugger di Visual Studio e ho visualizzato l'output x86 decompilato. In entrambi i casi, il codice x86 era identico, ad eccezione dei nomi delle classi e degli indirizzi di memoria delle funzioni (che ovviamente devono essere diversi). Ecco 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 

Ho quindi pensato che forse l'esecuzione sotto il debugger gli causasse un'ottimizzazione meno aggressiva?

Ho quindi eseguito un eseguibile di build di versione autonomo al di fuori di qualsiasi ambiente di debug e ho utilizzato WinDBG + SOS per intervenire dopo il completamento del programma e visualizzare lo smontaggio del codice x86 compilato JIT.

Come si può vedere dal codice seguente, quando si esegue all'esterno del debugger il compilatore JIT è più aggressivo e ha incorporato il WriteItmetodo direttamente nel chiamante. La cosa cruciale tuttavia è che era identico quando si chiamava una classe sigillata contro non sigillata. Non c'è alcuna differenza tra una classe sigillata o non sigillata.

Ecco 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 sigillata:

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 una prova concreta che non ci può essere alcun miglioramento delle prestazioni tra i metodi di chiamata su classi sigillate vs non sigillate ... Penso di essere felice ora :-)


Qual è il motivo per cui hai pubblicato questa risposta due volte invece di chiudere l'altro come duplicato? Mi sembrano duplicati validi anche se questa non è la mia area.
Flexo

1
Cosa succederebbe se i metodi fossero più lunghi (codice IL di oltre 32 byte) per evitare di inline e vedere quale operazione di chiamata verrebbe utilizzata? Se è in linea, non vedi la chiamata, quindi non puoi giudicare gli effetti.
giovedì

Sono confuso: perché c'è callvirtquando questi metodi non sono virtuali per cominciare?
strano

@freakish Non riesco a ricordare dove ho visto questo, ma ho letto che il CLR ha usato callvirtper i metodi sigillati perché deve ancora controllare null l'oggetto prima di invocare la chiamata del metodo, e una volta che lo hai considerato, potresti anche usare callvirt. Per rimuovere il callvirte semplicemente saltare direttamente, avrebbero bisogno di modificare C # per consentire ((string)null).methodCall()come fa C ++, o dovrebbero dimostrare staticamente che l'oggetto non era nullo (cosa che potevano fare, ma non si sono preoccupati di)
Orion Edwards,

1
Puntelli per lo sforzo di scavare a livello di codice macchina, ma fai molta attenzione a fare affermazioni come 'questo fornisce una prova concreta che non ci può essere alcun miglioramento delle prestazioni'. Quello che hai mostrato è che per uno scenario particolare non c'è differenza nell'output nativo. È un punto dati e non si può presumere che si generalizzi a tutti gli scenari. Per cominciare, le tue classi non definiscono alcun metodo virtuale, quindi le chiamate virtuali non sono richieste affatto.
Matt Craig,

24

Come so, non vi è alcuna garanzia di beneficio in termini di prestazioni. Ma c'è la possibilità di ridurre la penalità di prestazione in alcune condizioni specifiche con il metodo sigillato. (la classe sigillata rende sigillati tutti i metodi.)

Ma dipende dall'ambiente di implementazione e esecuzione del compilatore.


Dettagli

Molte CPU moderne utilizzano una struttura a pipeline lunga per aumentare le prestazioni. Poiché la CPU è incredibilmente più veloce della memoria, la CPU deve precaricare il codice dalla memoria per accelerare la pipeline. Se il codice non è pronto al momento opportuno, le tubazioni saranno inattive.

Esiste un grande ostacolo chiamato invio dinamico che interrompe questa ottimizzazione "prefetching". Puoi capirlo solo come una ramificazione 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();

La CPU non può precaricare il prossimo codice da eseguire in questo caso perché la posizione del codice successivo è sconosciuta fino alla risoluzione della condizione. Quindi questo rende il pericolo causa inattività della tubazione. E la penalità per le prestazioni al minimo è enorme nei normali.

Una cosa simile accade in caso di override del metodo. Il compilatore può determinare l'override del metodo corretto per la chiamata del metodo corrente, ma a volte è impossibile. In questo caso, il metodo corretto può essere determinato solo in fase di esecuzione. Questo è anche un caso di invio dinamico e, una delle ragioni principali delle lingue tipizzate dinamicamente è generalmente più lenta delle lingue tipizzate staticamente.

Alcune CPU (compresi i recenti chip x86 di Intel) utilizzano una tecnica chiamata esecuzione speculativa per utilizzare la pipeline anche sulla situazione. Basta precaricare uno dei percorsi di esecuzione. Ma il tasso di successo di questa tecnica non è così elevato. E il fallimento della speculazione provoca l'arresto della pipeline, che comporta anche un'enorme penalità delle prestazioni. (Questo è completamente implementato dalla CPU. Alcune CPU mobili sono conosciute come questo tipo di ottimizzazione per risparmiare energia)

Fondamentalmente, C # è un linguaggio compilato staticamente. Ma non sempre. Non conosco le condizioni esatte e questo dipende interamente dall'implementazione del compilatore. Alcuni compilatori possono eliminare la possibilità di invio dinamico impedendo la sostituzione del metodo se il metodo è contrassegnato come sealed. I compilatori stupidi no. Questo è il vantaggio prestazionale di sealed.


Questa risposta ( Perché è più veloce elaborare un array ordinato rispetto a un array non ordinato? ) Descrive molto meglio la previsione del ramo.


1
Le CPU di classe Pentium effettuano il prefetch della spedizione indiretta direttamente. A volte il reindirizzamento del puntatore a funzione è più veloce di se (non indovinabile) per questo motivo.
Joshua,

2
Un vantaggio della funzione non virtuale o sigillata è che possono essere incorporati in più situazioni.
CodesInChaos

4

<Off-topic-rant>

Io detesto classi sigillate. Anche se i vantaggi in termini di prestazioni sono sorprendenti (di cui dubito), distruggono il modello orientato agli oggetti impedendo il riutilizzo tramite eredità. Ad esempio, la classe Thread è sigillata. Mentre vedo che si potrebbe desiderare che i thread siano il più efficienti possibile, posso anche immaginare scenari in cui essere in grado di sottoclassare Thread avrebbe grandi vantaggi. Autori di classe, se è necessario sigillare le lezioni per motivi di "prestazioni", si prega di fornire almeno un'interfaccia in modo da non dover avvolgere e sostituire ovunque che abbiamo bisogno di una funzionalità che hai dimenticato.

Esempio: SafeThread ha dovuto avvolgere la classe Thread perché Thread è sigillato e non esiste un'interfaccia IThread; SafeThread intrappola automaticamente le eccezioni non gestite sui thread, qualcosa che manca completamente nella classe Thread. [e no, gli eventi di eccezione non gestiti non raccolgono eccezioni non gestite nei thread secondari].

</ Off-topic-rant>


35
Non sigillo le mie lezioni per motivi di performance. Li sigillo per motivi di design. Progettare per l'eredità è difficile e questo sforzo sarà sprecato per la maggior parte del tempo. Sono totalmente d'accordo sul fornire interfacce, questa è una soluzione di gran lunga superiore alle classi non sigillanti.
Jon Skeet,

6
L'incapsulamento è generalmente una soluzione migliore dell'eredità. Per prendere il tuo esempio di thread specifico, il trapping delle eccezioni del thread interrompe il Principio di sostituzione di Liskov perché hai modificato il comportamento documentato della classe Thread, quindi anche se potresti derivarne, non sarebbe ragionevole dire che potresti usare SafeThread ovunque potresti usare Thread. In questo caso, sarebbe meglio incapsulare Thread in un'altra classe che ha un comportamento documentato diverso, che è possibile eseguire. A volte le cose sono sigillate per il tuo bene.
Greg Beech,

1
@ [Greg Beech]: opinione, non fatto - essere in grado di ereditare da Thread per correggere una supervisione atroce nel suo design NON è una brutta cosa ;-) E penso che stai sopravvalutando LSP - la proprietà dimostrabile q (x) in questo caso è "un'eccezione non gestita che distrugge il programma" che non è una "proprietà desiderabile" :-)
Steven A. Lowe,

1
No, ma ho avuto la mia parte di codice scadente in cui ho permesso ad altri sviluppatori di abusare delle mie cose non sigillandole o consentendo casi d'angolo. La maggior parte del mio codice al giorno d'oggi è affermazioni e altre cose relative al contratto. E sono abbastanza aperto sul fatto che lo faccio solo per essere un dolore nel ***.
Turing completato il

2
Dal momento che qui stiamo svolgendo attività off-topic, proprio come detesti le lezioni sigillate, detesto le eccezioni ingoiate. Non c'è niente di peggio di quando qualcosa va a monte ma il programma continua. JavaScript è il mio preferito. Apporti una modifica ad un codice e improvvisamente facendo clic su un pulsante non fa assolutamente nulla. Grande! ASP.NET e UpdatePanel è un altro; seriamente, se il mio gestore di pulsanti lancia un grosso problema e ha bisogno di CRASH, quindi so che c'è qualcosa che deve essere riparato! Un pulsante che non fa nulla è più inutile di un pulsante che fa apparire una schermata di blocco!
Roman Starkov,

4

Contrassegnare una classe non sealeddovrebbe avere alcun impatto sulle prestazioni.

Ci sono casi in cui cscpotrebbe essere necessario emettere un callvirtopcode anziché un callopcode. Tuttavia, sembra che quei casi siano rari.

E mi sembra che la JIT dovrebbe essere in grado di emettere la stessa chiamata di funzione non virtuale per callvirtquella che farebbe call, se sa che la classe non ha sottoclassi (ancora). Se esiste una sola implementazione del metodo, non ha senso caricare il suo indirizzo da una vtable: basta chiamare direttamente l'implementazione. Del resto, il JIT può persino incorporare la funzione.

È un po 'una scommessa da parte della JIT, perché se una sottoclasse viene successivamente caricata, la JIT dovrà buttare via quel codice macchina e compilarlo di nuovo, emettendo una vera chiamata virtuale. Suppongo che questo non accada spesso in pratica.

(E sì, i progettisti di macchine virtuali perseguono davvero in modo aggressivo queste minuscole vittorie nelle prestazioni.)


3

Le classi sigillate dovrebbero fornire un miglioramento delle prestazioni. Poiché non è possibile derivare una classe sigillata, tutti i membri virtuali possono essere trasformati in membri non virtuali.

Certo, stiamo parlando di guadagni davvero piccoli. Non classificherei una classe come sigillata solo per ottenere un miglioramento delle prestazioni se la profilazione non lo rivelasse un problema.


Essi dovrebbero , ma sembra che non lo fanno. Se il CLR avesse il concetto di tipi di riferimento non nulli, allora una classe sigillata sarebbe davvero migliore, poiché il compilatore potrebbe emettere callinvece di callvirt... Mi piacerebbe anche i tipi di riferimento non nulli per molte altre ragioni ... sospiro :-(
Orion Edwards,

3

Considero le classi "sigillate" il caso normale e SEMPRE ho un motivo per omettere la parola chiave "sigillata".

I motivi più importanti per me sono:

a) Migliori controlli del tempo di compilazione (il casting su interfacce non implementate verrà rilevato al momento della compilazione, non solo in fase di esecuzione)

e, motivo principale:

b) Non è possibile abusare delle mie lezioni in questo modo

Vorrei che Microsoft avrebbe reso "sigillato" lo standard, non "non sigillato".


Credo che "omettere" (per lasciare fuori) dovrebbe essere "emettere" (per produrre)?
user2864740

2

@Vaibhav, che tipo di test hai eseguito per misurare le prestazioni?

Immagino che uno dovrebbe usare Rotor e approfondire la CLI e capire come una classe sigillata migliorerebbe le prestazioni.

SSCLI (Rotore)
SSCLI: infrastruttura di linguaggio comune di origine condivisa

Common Language Infrastructure (CLI) è lo standard ECMA che descrive il nucleo di .NET Framework. La CLI di origine condivisa (SSCLI), nota anche come Rotor, è un archivio compresso del codice sorgente per un'implementazione funzionante della CLI ECMA e della specifica del linguaggio ECMA C #, tecnologie alla base dell'architettura .NET di Microsoft.


Il test includeva la creazione di una gerarchia di classi, con alcuni metodi che stavano facendo un lavoro fittizio (principalmente manipolazione di stringhe). Alcuni di questi metodi erano virtuali. Si chiamavano qua e là. Quindi chiamando questi metodi 100, 10000 e 100000 volte ... e misurando il tempo trascorso. Quindi eseguendo questi dopo aver contrassegnato le classi come sigillate. e misurare di nuovo. Nessuna differenza in loro.
Vaibhav,

2

le classi sigillate saranno almeno un po 'più veloci, ma a volte possono essere più veloci ... se l'ottimizzatore JIT è in grado di incorporare chiamate che altrimenti sarebbero state chiamate virtuali. Quindi, dove ci sono spesso metodi abbastanza piccoli da essere integrati, prendi in considerazione l'idea di sigillare la classe.

Tuttavia, il miglior motivo per sigillare una classe è dire "Non ho progettato questo per essere ereditato, quindi non ti lascerò bruciare supponendo che sia stato progettato per essere così, e non lo farò bruciarmi rimanendo bloccato in un'implementazione perché ti lascio derivare da essa. "

So che alcuni qui hanno detto che odiano le classi sigillate perché vogliono l'opportunità di derivare da qualsiasi cosa ... ma questa è spesso la scelta non più mantenibile ... perché esporre una classe alla derivazione ti blocca molto più che non esporre tutto quello. È simile a dire "Odio le classi che hanno membri privati ​​... Spesso non riesco a fare in modo che la classe faccia quello che voglio perché non ho accesso." L'incapsulamento è importante ... la sigillatura è una forma di incapsulamento.


Il compilatore C # utilizzerà comunque callvirt(chiamata al metodo virtuale) per i metodi di istanza su classi sigillate, poiché deve ancora eseguire un controllo di oggetti null su di esse. Per quanto riguarda l'inline, il CLR JIT può (e fa) incorporare chiamate a metodi virtuali per classi sia sigillate che non sigillate ... quindi sì. La cosa della performance è un mito.
Orion Edwards,

1

Per vederli davvero è necessario analizzare il codice compilato JIT (ultimo).

Codice 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();
}

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

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

Mentre la creazione degli oggetti è la stessa, le istruzioni eseguite per invocare i metodi della classe sigillata e derivata / base sono leggermente diverse. Dopo aver spostato i dati in registri o RAM (istruzione mov), l'invocazione del metodo sigillato, esegue un confronto tra dword ptr [ecx], ecx (istruzione cmp) e quindi chiama il metodo mentre la classe derivata / base esegue direttamente il metodo. .

Secondo il rapporto scritto da Torbj¨orn Granlund, latenze di istruzione e throughput per processori AMD e Intel x86 , la velocità delle seguenti istruzioni in un Intel Pentium 4 sono:

  • mov : ha 1 ciclo come latenza e il processore può supportare 2,5 istruzioni per ciclo di questo tipo
  • cmp : ha 1 ciclo come latenza e il processore può supportare 2 istruzioni per ciclo di questo tipo

Link : https://gmplib.org/~tege/x86-timing.pdf

Ciò significa che, idealmente , il tempo necessario per invocare un metodo sigillato è di 2 cicli mentre il tempo necessario per invocare un metodo derivato o di classe base è di 3 cicli.

L'ottimizzazione dei compilatori ha fatto la differenza tra le prestazioni di una classe sigillata e non sigillata così bassa che stiamo parlando di cerchi di processori e per questo motivo sono irrilevanti per la maggior parte delle applicazioni.


-10

Esegui questo codice e vedrai che le classi sigillate sono 2 volte più veloci:

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";
    }
}

uscita: Classe sigillata: 00: 00: 00.1897568 Classe non sigillata: 00: 00: 00.3826678


9
Ci sono un paio di problemi con questo. Prima di tutto, non stai ripristinando il cronometro tra il primo e il secondo test. In secondo luogo, il modo in cui stai chiamando il metodo significa che tutti i codici operativi saranno chiamati non callvirt, quindi il tipo non ha importanza.
Cameron MacFarland,

1
RuslanG, Hai dimenticato di chiamare watch.Reset () dopo aver eseguito il primo test. o_O:]

Sì, il codice sopra ha dei bug. Inoltre, chi può vantarsi di non introdurre mai errori nel codice? Ma questa risposta ha una cosa importante meglio di tutte le altre: cerca di misurare l'effetto in questione. E questa risposta condivide anche il codice per tutti voi che ispezionate e [votate in massa] (mi chiedo perché sia ​​necessario il downgrade in massa?). Contrariamente alle altre risposte. A mio avviso, questo merita rispetto ... Si noti inoltre che l'utente è un principiante, quindi neanche un approccio molto accogliente. Alcune semplici correzioni e questo codice diventa utile per tutti noi. Correggi + condividi la versione fissa, se hai il coraggio
Roland Pihlakas l'

Non sono d'accordo con @RolandPihlakas. Come hai detto, "chi può vantarsi di non introdurre errori nel codice". Risposta -> No, nessuna. Ma non è questo il punto. Il punto è che si tratta di un'informazione errata che può fuorviare un altro nuovo programmatore. Molte persone possono facilmente perdere che il cronometro non è stato ripristinato. Potrebbero credere che le informazioni di riferimento siano vere. Non è più dannoso? Qualcuno che sta rapidamente cercando una risposta potrebbe anche non leggere questi commenti, piuttosto vedrebbe una risposta e potrebbe crederci.
Emran Hussain,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.