2009-05-26 10 views
33

¿Por qué los tipos sellados son más rápidos?¿Por qué los tipos sellados son más rápidos?

Me pregunto acerca de los detalles más profundos sobre por qué esto es cierto.

+2

Ver: http://stackoverflow.com/questions/268251/why-seal-a- clase – Shog9

+0

¿Lo son? No sé ... el CLR puede optimizar la tabla de envío de métodos, sabiendo que ya no puede crecer. – harpo

+1

@harpo: vea esta referencia: http://msdn.microsoft.com/en-us/library/ms173150.aspx. Lo agregué a mi respuesta, pero el simple hecho es que no dicen mucho sobre POR QUÉ, así que decidí no agregarlo ... –

Respuesta

36

En el nivel más bajo, el compilador puede hacer una micro-optimización cuando tiene clases selladas.

Si llama a un método en una clase sellada y el tipo se declara en tiempo de compilación para ser esa clase sellada, el compilador puede implementar la llamada a método (en la mayoría de los casos) utilizando la instrucción IL de llamada en lugar del instrucción callvirt IL. Esto se debe a que el objetivo del método no se puede anular. La llamada elimina una verificación nula y realiza una búsqueda vtable más rápida que callvirt, ya que no tiene que verificar las tablas virtuales.

Esto puede ser una mejora muy, muy leve en el rendimiento.

Dicho esto, lo ignoraría por completo al decidir si se sella una clase. Marcar un tipo sellado realmente debe ser una decisión de diseño, no una decisión de rendimiento. ¿Deseas que las personas (incluyéndote a ti) subclases potenciales de tu clase, ahora o en el futuro? Si es así, no selles. Si no, sella. Ese realmente debería ser el factor decisivo.

+4

Al diseñar, puede ser una buena idea inclinarse hacia el sellado de tipos públicos que no necesitan ampliarse explícitamente, ya que desbloquear una clase en una versión futura es un cambio sin interrupción, mientras que lo contrario no es cierto. –

+1

@Neil Williams: Estoy de acuerdo. En general, dado que desbloquear una clase es seguro, y el sellado no lo es, si está haciendo bibliotecas públicas, el sellado puede ser algo bueno de hacer. De nuevo, sin embargo, esto hace que sellar una opción de diseño sea más que un problema de rendimiento. –

+0

Pensé que era debido a la creación de líneas. El compilador C# siempre usa callvirt porque le gusta el efecto secundario null-check de ese código IL. –

9

Básicamente, tiene que ver con el hecho de que no necesitan preocuparse por las extensiones de una tabla de funciones virtuales; los tipos sellados no se pueden extender, y por lo tanto, el tiempo de ejecución no necesita preocuparse acerca de cómo pueden ser polimórficos.

5

Si el compilador JIT ve una llamada a un método virtual utilizando tipos sellados, puede producir un código más eficiente llamando al método de forma no virtual. Ahora, llamar a un método no virtual es más rápido porque no es necesario realizar una búsqueda vtable. En mi humilde opinión, esta es la optimización de micro que debe utilizarse como último recurso para mejorar el rendimiento de una aplicación. Si su método contiene cualquier código, la versión virtual será más lenta que la no virtual en comparación con el costo de ejecutar el código.

+2

¿Por qué debería? este es un último recurso? ¿Por qué no simplemente sellar tus clases por defecto? Por lo general, solo se considera una microopimización si hay algún costo asociado con ella (código menos legible, o más tiempo de desarrollo típicamente). Si no hay inconvenientes para hacerlo, ¿por qué no hacerlo si el rendimiento es o no un problema? – jalf

+1

Cuando sella una clase, evita el uso de la herencia. Esto puede dificultar el desarrollo, puede evitar trabajar alrededor de ciertos errores. Idealmente, uno pensaría en ello y diseñaría para la herencia, y aclararía lo que está diseñado para extenderse y sellar todo lo demás. Cegar ciegamente todo es demasiado restrictivo. – Eddie

3

Para ampliar las respuestas de otros, una clase sellada (el equivalente a una clase final en Java) no se puede extender. Esto significa que cada vez que el compilador ve que se usa un método de esta clase, el compilador sabe absolutamente que no se necesita el despacho en tiempo de ejecución. No tiene que examinar la clase para ver dinámicamente qué método de qué clase en la jerarquía necesita ser llamado. Esto significa que la rama se puede compilar en lugar de ser dinámica.

Por ejemplo, si tengo una clase no sellada Animal que tiene un método makeNoise(), el compilador no necesariamente saber si o no cualquier Animal instancia anula ese método. Por lo tanto, cada vez que una instancia Animal invoque makeNoise(), se debe verificar la jerarquía de clases de la instancia para ver si la instancia anula este método en una clase extendida.

Sin embargo, si tengo una clase sellada AnimalFeeder que tiene un método feedAnimal(), entonces el compilador sabe con certeza que este método no se puede anular. Puede compilar en una rama a subrutina o instrucción equivalente en lugar de usar una tabla de despacho virtual.

Nota: Puede utilizar sealed en una clase para evitar cualquier herencia de esa clase, y se puede utilizar en un método sealed que fue declarado virtual en una clase básica para evitar más primordial de ese método.

8

Decidió publicar pequeñas muestras de código para ilustrar cuándo el compilador C# emite "call" & instrucciones "callvirt".

tanto, aquí está el código fuente de todos los tipos que he utilizado:

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

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

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

También tengo un método que llama a todos "DoSmth()" métodos:

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

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

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

Mirando en "Call() "método podemos decir que (teóricamente) el compilador de C# debe emitir 2" callvirt "& 1 instrucciones de" llamada ", ¿verdad? Por desgracia, la realidad es un poco diferente - 3 -s "callvirt":

.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 razón es bastante simple: tiempo de ejecución debe comprobar si instancia de tipo no es igual a cero antes de llamar al método "DoSmth()". PERO todavía podemos escribir nuestro código de una manera tal que compilador de C# sería capaz de emitir optimizado código IL:

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

     new ClassWithVirtualMethod().DoSmth(); 

     new ClassWithSealedMethod().DoSmth(); 
    } 

El resultado es:

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

Si se intenta llamar no -método virtual de clase no sellada de la misma manera que también recibirá instrucciones de "llamada" en lugar de "callvirt"

+0

Gracias, ¿por qué se evita la verificación nula en su segundo ejemplo? ¿Puede usted explicar por favor? –

+1

Porque no utilizo la variable local del tipo "SealedClass" como en el primer ejemplo, por lo que el compilador no necesita verificar si es 'nulo'. Se generará el mismo código IL si declara que el método "SealedClass.DoSmth()" es estático –

Cuestiones relacionadas