Pregunta

Me he encontrado con muchos consejos de optimización que dicen que debes marcar tus 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 realizado pruebas y ha visto una diferencia?

Ayúdame a aprender :)

¿Fue útil?

Solución

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

Existen reglas complejas con respecto al tipo de llamada, virtual/no virtual, y no las conozco todas, por lo que no puedo describírselas, 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 obtenga de este nivel de optimización debe considerarse como último recurso; optimice siempre 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

Otros consejos

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

La cuestión se reduce a la call vs callvirt Códigos de operación de IL. Call es más rápido que callvirt, y callvirt se utiliza principalmente cuando no se sabe si el objeto ha sido subclasificado.Entonces la gente asume que si sella una clase, todos los códigos de operación cambiarán de calvirts a calls y será más rápido.

Desafortunadamente callvirt hace otras cosas que también lo hacen útil, como comprobar si hay referencias nulas.Esto significa que incluso si una clase está sellada, la referencia aún podría ser nula y, por lo tanto, una callvirt es necesario.Puedes solucionar esto (sin necesidad de sellar la clase), pero se vuelve un poco inútil.

Uso de estructuras call porque no se pueden subclasificar y nunca son nulos.

Consulte esta pregunta para obtener más información:

Llamar y llamarvirt

Actualizar:A partir de .NET Core 2.0 y .NET Desktop 4.7.1, CLR ahora admite la desvirtualizació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 descubre que es seguro hacerlo.

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

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

https://blogs.msdn.microsoft.com/dotnet/2017/06/29/rendimiento-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 se emitió.

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 compilación de lanzamiento) 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 por la que la gente dice que el sellado proporciona beneficios de rendimiento es que el compilador sabe que la clase no se anula y, por lo tanto, puede usar call en lugar de callvirt ya que no tiene que buscar virtuales, etc.Como se demostró anteriormente, esto no es cierto.

Mi siguiente pensamiento fue que, aunque MSIL es idéntico, ¿tal vez el compilador JIT trata las clases selladas de manera diferente?

Ejecuté una versión de lanzamiento con 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 clases y las direcciones de memoria de funciones (que por supuesto deben ser diferentes).Aquí lo tienes

//            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 el depurador provoca que realice una optimización menos agresiva.

Luego ejecuté una versión ejecutable de versión independiente fuera de cualquier entorno de depuración y usé WinDBG + SOS para ingresar después de que se completó el programa y ver el desmontaje del código x86 compilado JIT.

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

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 hay no puedo Habrá alguna mejora en el rendimiento entre llamar a métodos en clases selladas y no selladas...Creo que ahora soy feliz :-)

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

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


Detalles

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

Hay un gran obstáculo llamado despacho dinámico lo que interrumpe esta optimización de "captación previa".Puedes entender esto simplemente como una bifurcación condicional.

// 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 no puede precargar el siguiente código para ejecutar en este caso porque la posición del siguiente código se desconoce hasta que se resuelva la condición.Entonces esto hace peligro provoca que la tubería esté inactiva.Y la penalización en el rendimiento por inactividad es enorme en modo normal.

Sucede algo similar en caso de anulación de método.El compilador puede determinar la anulación del método adecuado para la llamada al método actual, pero a veces es imposible.En este caso, el método adecuado sólo se puede determinar en tiempo de ejecución.Este también es un caso de despacho dinámico y una de las principales razones por las que los lenguajes de tipado dinámico son generalmente más lentos que los lenguajes de tipado estático.

Algunas CPU (incluidos los chips x86 recientes de Intel) utilizan una técnica llamada ejecución especulativa utilizar tuberías incluso en la situación.Simplemente precarga una de las rutas de ejecución.Pero la tasa de acierto de esta técnica no es tan alta.Y el fracaso de la especulación provoca una parada en el oleoducto, lo que también genera una enorme penalización en el rendimiento.(Esto se debe completamente a la implementación de la CPU.Se sabe que algunas CPU móviles no tienen este tipo de optimización para ahorrar energía)

Básicamente, C# es un lenguaje compilado estáticamente.Pero no siempre.No sé la condición exacta y esto depende totalmente de la implementación del compilador.Algunos compiladores pueden eliminar la posibilidad de envío dinámico evitando la anulación de métodos si el método está marcado como sealed.Es posible que los compiladores estúpidos no lo hagan.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 mucho mejor la predicción de la rama.

Marcando 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 código de operación en lugar de un call código de operación.Sin embargo, parece que esos casos son raros.

Y me parece que el JIT debería poder emitir la misma llamada de función no virtual para callvirt que sería para call, si sabe que la clase no tiene subclases (todavía).Si solo existe una implementación del método, no tiene sentido cargar su dirección desde una vtable; simplemente llame a esa implementación directamente.De hecho, el JIT puede incluso incorporar la función.

Es un poco arriesgado por parte del JIT, porque si una subclase es Luego cargado, el JIT tendrá que desechar ese código de máquina y compilar el código nuevamente, emitiendo una llamada virtual real.Supongo que esto no sucede a menudo en la práctica.

(Y sí, los diseñadores de VM realmente persiguen agresivamente estas pequeñas mejoras en el rendimiento).

Clases selladas debería proporcionar una mejora del rendimiento.Dado que no se puede derivar una clase sellada, cualquier miembro virtual puede convertirse en miembro no virtual.

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

<fuera de tema-despotricar>

I detestar clases selladas.Incluso si los beneficios de rendimiento son asombrosos (lo cual dudo), destruir el modelo orientado a objetos evitando la reutilización por herencia.Por ejemplo, la clase Thread está sellada.Si bien puedo ver que uno podría querer que los subprocesos sean lo más eficientes posible, también puedo imaginar escenarios en los que poder subclasificar Thread tendría grandes beneficios.Autores de clase, si debe sella tus clases por motivos de "rendimiento", por favor proporcione una interfaz al menos para que no tengamos que ajustar y reemplazar en todos los lugares donde necesitamos una característica que olvidó.

Ejemplo: hilo seguro Tuve que ajustar la clase Thread porque Thread está sellado y no hay una interfaz IThread;SafeThread atrapa automáticamente excepciones no controladas en subprocesos, algo que falta por completo en la clase Thread.[y no, los eventos de excepción no controlados sí lo hacen no detectar excepciones no controladas en subprocesos secundarios].

</off-topic-rant>

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

Ojalá Microsoft hubiera hecho "sellado" el estándar, no "abierto".

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

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

SSCLI (rotor)
SSCLI:Infraestructura de lenguaje común de fuente compartida

La infraestructura del lenguaje común (CLI) es el estándar ECMA que describe el núcleo del marco .NET.La CLI de fuente compartida (SSCLI), también conocida como rotor, es un archivo comprimido del código fuente para una implementación de trabajo de la CLI ECMA y la especificación del lenguaje ECMA C#, tecnologías en el corazón de la arquitectura .NET de Microsoft.

Las clases selladas serán al menos un poquito más rápidas, pero a veces pueden ser muchísimo más rápidas...si JIT Optimizer puede integrar llamadas que de otro modo habrían sido llamadas virtuales.Entonces, cuando haya métodos llamados con frecuencia que sean lo suficientemente pequeños como para incluirlos 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, 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 dejé derivar de ella".

Sé que algunos aquí han dicho que odian las clases selladas porque quieren tener la oportunidad de derivar de cualquier cosa...pero A MENUDO esa no es la opción más fácil de mantener...porque exponer una clase a la derivación te bloquea en mucho más que no exponer todo eso.Es similar a decir "Odio 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.

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

producción:Clase sellada:00: 00: 00.1897568 Clase no seleccionada:00:00:00.3826678

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top