2009-05-26 44 views
33

Perché i tipi sigillati sono più veloci?Perché i tipi sigillati sono più veloci?

mi chiedo circa i più profondi dettagli sul motivo per cui questo è vero.

+2

Si prega di consultare: http://stackoverflow.com/questions/268251/why-seal-a- classe – Shog9

+0

Sono loro? Non lo so ... il CLR potrebbe essere in grado di ottimizzare la tabella di invio del metodo, sapendo che non può crescere più. – harpo

+1

@harpo: vedere questo riferimento: http://msdn.microsoft.com/en-us/library/ms173150.aspx. Ho aggiunto alla mia risposta, ma il semplice fatto è che non dicono molto su PERCHÉ, quindi ho deciso di non aggiungerlo ... –

risposta

36

Al livello più basso, il compilatore può fare una micro-ottimizzazione quando si hanno sigillato le classi.

Se si chiama un metodo su una classe sigillata e il tipo viene dichiarato in fase di compilazione per essere quella classe sigillata, il compilatore può implementare la chiamata al metodo (nella maggior parte dei casi) utilizzando l'istruzione IL chiamata invece del callvirt IL istruzione. Questo perché il metodo target non può essere sovrascritto. Call elimina un controllo nullo e fa una ricerca vtable più veloce di callvirt, poiché non deve controllare le tabelle virtuali.

Questo può essere un miglioramento molto, molto lieve delle prestazioni.

Detto questo, avrei completamente ignorare quel momento di decidere se per sigillare una classe. Contrassegnare un tipo sigillato in realtà dovrebbe essere una decisione di progettazione, non una decisione di prestazione. Vuoi che le persone (incluso te stesso) siano potenzialmente sottoclasse dalla tua classe, ora o in futuro? Se è così, non sigillare. Altrimenti, sigillo. Questo dovrebbe essere il fattore decisivo.

+4

Quando si progetta, può essere una buona idea inclinarsi a sigillare tipi pubblici che non devono essere esplicitamente estesi, poiché la rimozione di una classe in una versione futura è un cambiamento continuo mentre il contrario non è vero. –

+1

@Neil Williams: sono d'accordo. In generale, dal momento che la rimozione di una classe è sicura e la chiusura non lo è, se si stanno creando biblioteche pubbliche, la chiusura può essere una buona cosa da fare. Di nuovo, tuttavia, questo rende la scelta del design più vincolante rispetto a un problema di prestazioni. –

+0

Ho pensato che fosse dovuto alla funzione di inlining. Il compilatore C# usa sempre callvirt perché gli piace l'effetto collaterale null-check di quel codice IL. –

9

In sostanza, è ottenuto a che fare con il fatto che essi non hanno bisogno di preoccuparvi di estensioni per una tabella di funzione virtuale; i tipi sigillati non possono essere estesi e, pertanto, il runtime non deve preoccuparsi di come potrebbero essere polimorfici.

5

Se il compilatore JIT vede una chiamata a un metodo virtuale utilizzando una tipi sigillati può produrre codice più efficiente chiamando il metodo non virtualmente. Ora chiamare un metodo non virtuale è più veloce perché non è necessario eseguire una ricerca vtable. IMHO questa è micro ottimizzazione che dovrebbe essere utilizzata come ultima risorsa per migliorare le prestazioni di un'applicazione. Se il tuo metodo contiene qualsiasi codice, la versione virtuale sarà trascurabilmente più lenta del non-virtuale rispetto al costo dell'esecuzione del codice stesso.

+2

Perché dovrebbe questa è l'ultima risorsa? Perché non solo sigillare le classi per impostazione predefinita? Solitamente viene considerato una microopimizzazione solo se è associato un costo (codice meno leggibile o più tempo di sviluppo in genere). Se non ci sono aspetti negativi nel farlo, perché non farlo indipendentemente dal fatto che la prestazione sia un problema? – jalf

+1

Quando si sigilla una classe, si impedisce l'utilizzo dell'ereditarietà. Ciò può rendere più difficile lo sviluppo, può impedire di aggirare alcuni bug. Idealmente, ci si potrebbe pensare e progettare per ereditarietà, e rendere chiaro ciò che è progettato per essere esteso e sigillare tutto il resto. Sigillare ciecamente tutto è troppo restrittivo. – Eddie

3

Per estendere le risposte degli altri, una classe chiusa (l'equivalente di una classe finale in Java) non può essere esteso. Ciò significa che ogni volta che il compilatore vede un metodo di questa classe essere utilizzato, il compilatore sa assolutamente che non è necessario alcun dispendio di runtime. Non è necessario esaminare la classe per vedere in modo dinamico quale metodo è necessario chiamare la classe nella gerarchia. Ciò significa che il ramo può essere compilato piuttosto che dinamico.

Ad esempio, se ho una classe non saldata Animal che ha un metodo makeNoise(), il compilatore non necessariamente sapere se qualsiasi Animal istanza sovrascrive tale metodo. Pertanto, ogni volta che un'istanza Animal richiama makeNoise(), la gerarchia di classi dell'istanza deve essere verificata per verificare se l'istanza sostituisce questo metodo in una classe estesa.

Tuttavia, se ho una classe sigillata AnimalFeeder con un metodo feedAnimal(), il compilatore sa con certezza che questo metodo non può essere sovrascritto. Può compilare in un ramo la subroutine o un'istruzione equivalente anziché utilizzare una tabella di distribuzione virtuale.

Nota: È possibile utilizzare sealed su una classe per evitare qualsiasi eredità di quella classe, ed è possibile utilizzare sealed su un metodo che è stato dichiarato virtual in una classe base per prevenire ulteriori prioritario di tale metodo.

8

Deciso di inviare piccoli esempi di codice per illustrare quando il compilatore C# emette le istruzioni "call" & "callvirt".

Quindi, ecco il codice sorgente di tutti i tipi, che ho usato:

public sealed class SealedClass 
    { 
     public void DoSmth() 
     { } 
    } 

    public class ClassWithSealedMethod : ClassWithVirtualMethod 
    { 
     public sealed override void DoSmth() 
     { } 
    } 

    public class ClassWithVirtualMethod 
    { 
     public virtual void DoSmth() 
     { } 
    } 

anche io ho un metodo che chiama tutti "DoSmth()" metodi:

public void Call() 
    { 
     SealedClass sc = new SealedClass(); 
     sc.DoSmth(); 

     ClassWithVirtualMethod cwcm = new ClassWithVirtualMethod(); 
     cwcm.DoSmth(); 

     ClassWithSealedMethod cwsm = new ClassWithSealedMethod(); 
     cwsm.DoSmth(); 
    } 

cerca su "Call() "metodo possiamo dire che (in teoria) il compilatore C# dovrebbe emettere 2" callvirt "& 1" call "istruzioni, giusto? Purtroppo, la realtà è un po 'diverso - 3 "callvirt" -s:

.method public hidebysig instance void Call() cil managed 
{ 
    .maxstack 1 
    .locals init (
     [0] class TestApp.SealedClasses.SealedClass sc, 
     [1] class TestApp.SealedClasses.ClassWithVirtualMethod cwcm, 
     [2] class TestApp.SealedClasses.ClassWithSealedMethod cwsm) 
    L_0000: newobj instance void TestApp.SealedClasses.SealedClass::.ctor() 
    L_0005: stloc.0 
    L_0006: ldloc.0 
    L_0007: callvirt instance void TestApp.SealedClasses.SealedClass::DoSmth() 
    L_000c: newobj instance void TestApp.SealedClasses.ClassWithVirtualMethod::.ctor() 
    L_0011: stloc.1 
    L_0012: ldloc.1 
    L_0013: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth() 
    L_0018: newobj instance void TestApp.SealedClasses.ClassWithSealedMethod::.ctor() 
    L_001d: stloc.2 
    L_001e: ldloc.2 
    L_001f: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth() 
    L_0024: ret 
} 

La ragione è molto semplice: il runtime deve verificare se il tipo di istanza non è uguale a zero prima di chiamare il metodo "DoSmth()". MA possiamo ancora scrivere il nostro codice in modo tale che il compilatore C# sarebbe in grado di emettere codice IL ottimizzato:

public void Call() 
    { 
     new SealedClass().DoSmth(); 

     new ClassWithVirtualMethod().DoSmth(); 

     new ClassWithSealedMethod().DoSmth(); 
    } 

risultato è:

.method public hidebysig instance void Call() cil managed 
{ 
    .maxstack 8 
    L_0000: newobj instance void TestApp.SealedClasses.SealedClass::.ctor() 
    L_0005: call instance void TestApp.SealedClasses.SealedClass::DoSmth() 
    L_000a: newobj instance void TestApp.SealedClasses.ClassWithVirtualMethod::.ctor() 
    L_000f: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth() 
    L_0014: newobj instance void TestApp.SealedClasses.ClassWithSealedMethod::.ctor() 
    L_0019: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth() 
    L_001e: ret 
} 

Se si tenta di chiamare non -Metodo virtuale di classe non sigillata allo stesso modo si otterrà anche l'istruzione "call" invece di "callvirt"

+0

Grazie, perché il controllo null viene evitato nel secondo esempio? Puoi spiegare per favore? –

+1

Perché non uso la variabile locale di tipo "SealedClass" come nel primo esempio, quindi il compilatore non ha bisogno di controllare se è "null". Lo stesso codice IL verrà generato se si dichiara il metodo "SealedClass.DoSmth()" come statico –

Problemi correlati