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

È stato utile?

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:

Chiamata e callvirt

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:

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


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 Infrastructure

Il 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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top