¿Las clases selladas realmente ofrecen beneficios de rendimiento?


Me he encontrado con muchos consejos de optimización que dicen que debe marcar sus clases como selladas para obtener beneficios de rendimiento adicionales.

Realicé algunas pruebas para comprobar el diferencial de rendimiento y no encontré ninguna. Estoy haciendo algo mal? ¿Me estoy perdiendo el caso en el que las clases selladas darán mejores resultados?

¿Alguien ha hecho pruebas y ha visto una diferencia?

Ayúdame a aprender:)

Author: Lance Roberts, 2008-08-05

11 answers

El JITter a veces utilizará llamadas no virtuales a métodos en clases selladas, ya que no hay forma de que se puedan extender más.

Hay reglas complejas con respecto al tipo de llamada, virtual/no virtual, y no las conozco todas, así que realmente no puedo describirlas por usted, pero si busca en Google clases selladas y métodos virtuales, puede encontrar algunos artículos sobre el tema.

Tenga en cuenta que cualquier tipo de beneficio de rendimiento que obtendría de este nivel de optimización debe ser considerado como último recurso, siempre optimice en el nivel algorítmico antes de optimizar en el nivel de código.

Aquí hay un enlace que menciona esto: Divagando sobre la palabra clave sellada

 50
Author: Lasse Vågsæther Karlsen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-08-05 12:37:05

La respuesta es no, las clases selladas no funcionan mejor que las no selladas.

El problema se reduce a los códigos call vs callvirt IL op. Call es más rápido que callvirt, y callvirt se utiliza principalmente cuando no se sabe si el objeto ha sido subclases. Así que la gente asume que si sellas una clase todos los códigos op cambiarán de calvirts a calls y serán más rápidos.

Desafortunadamente callvirt hace otras cosas que lo hacen útil también, como verificar las referencias nulas. Esto significa que incluso si una clase está sellada, la referencia podría seguir siendo null y, por lo tanto, se necesita un callvirt. Puedes evitar esto (sin necesidad de sellar la clase), pero se vuelve un poco inútil.

Las estructuras usan call porque no pueden ser subclases y nunca son null.

Vea esta pregunta para más información:

Call and callvirt

 135
Author: Cameron MacFarland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:31:14

Como sé, no hay garantía de beneficio de rendimiento. Pero existe una posibilidad de disminuir la penalización de rendimiento bajo alguna condición específica con el método sellado. (la clase sellada hace que todos los métodos sean sellados.)

Pero depende del entorno de implementación y ejecución del compilador.


Detalles

Muchas de las CPU modernas utilizan una estructura de tubería larga para aumentar el rendimiento. Debido a que la CPU es increíblemente más rápida que la memoria, la CPU tiene que precomprimir el código de la memoria para acelerar la tubería. Si el código no está listo en el momento adecuado, las canalizaciones estarán inactivas.

Hay un gran obstáculo llamado envío dinámico lo que interrumpe esta optimización de 'prefetching'. Puedes entender esto como una ramificación condicional.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know code to prefetch,
// so just prefetch one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

La CPU no puede prefetchear el siguiente código a ejecutar en este caso porque la siguiente posición del código es desconocida hasta que se resuelva la condición. Así que esto hace que hazard cause que la tubería esté inactiva. Y rendimiento la penalización por inactividad es enorme en regular.

Algo similar sucede en caso de sobreescritura de método. El compilador puede determinar la sobreescritura del método adecuado para la llamada al método actual, pero a veces es imposible. En este caso, el método adecuado solo se puede determinar en tiempo de ejecución. Este es también un caso de despacho dinámico, y, una razón principal de los lenguajes tipeados dinámicamente son generalmente más lentos que los lenguajes tipeados estáticamente.

Algunas CPU (incluidos los chips x86 de Intel recientes) utilizan una técnica llamada ejecución especulativa para utilizar la tubería incluso en la situación. Sólo prefetch uno de ruta de ejecución. Pero la tasa de éxito de esta técnica no es tan alta. Y el fracaso de la especulación causa el estancamiento de la tubería que también hace una gran penalización en el rendimiento. (esto es completamente por implementación de CPU. algunos CPU móvil se conoce como no este tipo de optimización para ahorrar energía)

Básicamente, C# es un lenguaje compilado estáticamente. Pero no siempre. No se condición exacta y esto depende completamente de la implementación del compilador. Algunos compiladores pueden eliminar la posibilidad de envío dinámico al evitar la sobreescritura del método si el método está marcado como sealed. Los compiladores estúpidos no pueden. Este es el beneficio de rendimiento del sealed.


Esta respuesta ( ¿Por qué es más rápido procesar una matriz ordenada que una matriz sin clasificar?) describe la predicción de ramas mucho mejor.

 21
Author: Eonil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:18

Actualización: A partir de.NET Core 2.0 y. NET Desktop 4.7.1, el CLR ahora admite la devirtualización. Puede tomar métodos en clases selladas y reemplazar llamadas virtuales con llamadas directas , y también puede hacer esto para clases no selladas si puede descubrir que es seguro hacerlo.

En tal caso (una clase sellada que el CLR no podría detectar como segura para desvirtualizar), una clase sellada realmente debería ofrecer algún tipo de beneficio de rendimiento.

Dicho esto, no pensaría que sería vale la pena preocuparse por a menos que ya había perfilado el código y determinado que estaba en un camino particularmente caliente siendo llamado millones de veces, o algo así:

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


Respuesta original:

Hice el siguiente programa de prueba, y luego lo descompilé usando Reflector para ver qué código MSIL era emitir.

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

En todos los casos, el compilador de C # (Visual studio 2010 en la configuración de Release build) emite MSIL idéntico, que es el siguiente:

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 

La razón frecuentemente citada que la gente dice que sealed proporciona beneficios de rendimiento es que el compilador sabe que la clase no está anulada, y por lo tanto puede usar call en lugar de callvirt ya que no tiene que verificar virtuals, etc. Como se ha demostrado anteriormente, esto no es cierto.

Mi siguiente pensamiento fue que a pesar de que el MSIL es idéntico, ¿quizás el compilador JIT trata las clases selladas de manera diferente?

Ejecuté una compilación de lanzamiento bajo el depurador de visual studio y vi la salida x86 descompilada. En ambos casos, el código x86 era idéntico, con la excepción de los nombres de clase y las direcciones de memoria de las funciones (que, por supuesto, deben ser diferentes). Aquí está

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

Entonces pensé que tal vez ejecutar bajo el depurador hace que realice una optimización menos agresiva?

Luego ejecuté un ejecutable de compilación de versión independiente fuera de cualquier entorno de depuración, y usó WinDbg + SOS para entrar después de que el programa se hubiera completado, y ver el desmontaje del código x86 compilado por JIT.

Como puede ver en el código a continuación, cuando se ejecuta fuera del depurador, el compilador JIT es más agresivo, y ha insertado el método WriteIt directamente en el llamador. Sin embargo, lo crucial es que era idéntico al llamar a una clase sellada vs no sellada. No hay diferencia alguna entre un sellado o no sellado clase.

Aquí está cuando se llama a una clase normal:

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 clase sellada:

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

Para mí, esto proporciona una prueba sólida de que no puede haber ninguna mejora de rendimiento entre los métodos de llamada en clases selladas vs no selladas... Creo que ahora estoy feliz: -)

 19
Author: Orion Edwards,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-14 23:11:31

La calificación de una clase sealed no debería tener ningún impacto en el rendimiento.

Hay casos en los que csc podría tener que emitir un callvirt opcode en lugar de un call opcode. Sin embargo, parece que esos casos son raros.

Y me parece que el JIT debería ser capaz de emitir la misma llamada de función no virtual para callvirt que lo haría para call, si sabe que la clase no tiene ninguna subclase (todavía). Si solo existe una implementación del método, no tiene sentido cargar su dirección desde un vtable - simplemente llame directamente a la implementación one. Para el caso, el JIT puede incluso en línea la función.

Es un poco arriesgado por parte del JIT, porque si una subclase es cargada más tarde, el JIT tendrá que tirar ese código máquina y compilar el código nuevamente, emitiendo una llamada virtual real. Mi conjetura es que esto no sucede a menudo en la práctica.

(Y sí, los diseñadores de máquinas virtuales realmente persiguen agresivamente estas pequeñas victorias de rendimiento.)

 4
Author: Jason Orendorff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-20 19:16:32

Las clases selladas deberían proporcionar una mejora del rendimiento. Dado que no se puede derivar una clase sellada, cualquier miembro virtual se puede convertir en miembros no virtuales.

Por supuesto, estamos hablando de ganancias muy pequeñas. No marcaría una clase como sellada solo para obtener una mejora de rendimiento a menos que el perfil revelara que es un problema.

 3
Author: Karl Seguin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-08-05 12:37:07

I detesto las clases selladas. Incluso si los beneficios de rendimiento son asombrosos (lo que dudo), destruyen el modelo orientado a objetos al evitar la reutilización a través de la herencia. Por ejemplo, la clase Thread está sellada. Si bien puedo ver que uno podría querer hilos para ser lo más eficiente posible, también puedo imaginar escenarios donde ser capaz de subclase Hilo tendría grandes beneficios. Autores de clase, si debe sellar sus clases para razones de "rendimiento", por favor, proporcione una interfaz como mínimo para que no tengamos que empaquetar y reemplazar en todas partes que necesitamos una característica que olvidó.

Ejemplo: SafeThread tuvo que envolver la clase Thread porque Thread está sellado y no hay interfaz IThread; SafeThread atrapa automáticamente las excepciones no manejadas en los threads, algo que falta completamente en la clase Thread. [y no, los eventos de excepción no manejados hacen no recoger excepciones no manejadas en hilos secundarios].

 3
Author: Steven A. Lowe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-10-14 20:15:12

Considero que las clases "selladas" son el caso normal y SIEMPRE tengo una razón para omitir la palabra clave "sellada".

Las razones más importantes para mí son:

A) Mejores comprobaciones del tiempo de compilación (la conversión a interfaces no implementadas se detectará en tiempo de compilación, no solo en tiempo de ejecución)

Y, razón principal:

B) El abuso de mis clases no es posible de esa manera

Desearía que Microsoft hubiera hecho "sellado" el estándar, no "no sellado".

 3
Author: Turing Complete,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-03 14:27:15

@Vaibhav, ¿qué tipo de pruebas ejecutó para medir el rendimiento?

Supongo que uno tendría que usar Rotor y profundizar en CLI y entender cómo una clase sellada mejoraría el rendimiento.

SSCLI (Rotor)
SSCLI: Infraestructura de Lenguaje Común de Código Compartido

La Infraestructura del Lenguaje Común (CLI) es el estándar ECMA que describe el núcleo del. NET Marco. La CLI de Origen Compartido (SSCLI), también conocido como Rotor, es un archivo comprimido del código fuente a una implementación de trabajo de la ECMA CLI y el lenguaje C # de ECMA especificación, tecnologías en el corazón de. NET de Microsoft arquitectura.

 2
Author: hitec,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-08-05 12:40:10

Las clases selladas serán al menos un poco más rápidas, pero a veces pueden ser waayyy más rápidas... si el Optimizador JIT puede insertar llamadas que de otro modo habrían sido llamadas virtuales. Por lo tanto, donde hay métodos a menudo llamados que son lo suficientemente pequeños como para estar en línea, definitivamente considere sellar la clase.

Sin embargo, la mejor razón para sellar una clase es decir " No diseñé esto para ser heredado de, así que no voy a dejar que te quemes asumiendo que fue diseñado para ser así, y no voy a quemarme al quedar atrapado en una implementación porque te dejo derivar de ella."

Sé que algunos aquí han dicho que odian las clases selladas porque quieren la oportunidad de derivar de cualquier cosa... pero esa no es A MENUDO la opción más mantenible... porque exponer una clase a derivación te bloquea mucho más que no exponer todo eso. Es similar a decir " Detesto las clases que tienen miembros privados... A menudo no puedo hacer que la clase haga lo que quiero porque no tengo acceso."La encapsulación es importante... el sellado es una forma de encapsulación.

 2
Author: Brian Kennedy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-01 10:37:28

Ejecute este código y verá que las clases selladas son 2 veces más rápidas:

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

Salida: Clase sellada: 00: 00: 00.1897568 Clase no sellada:00: 00: 00.3826678

 -9
Author: RuslanG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-26 17:58:10