¿Las clases selladas realmente ofrecen beneficios de rendimiento?

140

He encontrado muchos consejos de optimización que dicen que debes marcar tus clases como selladas para obtener beneficios de rendimiento adicionales.

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

¿Alguien ha realizado pruebas y ha visto una diferencia?

Ayudame a aprender :)

Vaibhav
fuente
9
No creo que las clases selladas tuvieran la intención de aumentar el rendimiento. El hecho de que lo hagan puede ser incidental. Además de eso, perfile su aplicación después de haberla refactorizado para usar clases selladas y determine si valió la pena el esfuerzo. Bloquear su extensibilidad para realizar una micro optimización innecesaria le costará a largo plazo. Por supuesto, si realiza un perfil y le permite alcanzar sus puntos de referencia de rendimiento (en lugar de perf por el bien de perf), entonces puede tomar una decisión como equipo si vale la pena el dinero gastado. Si tiene clases selladas por razones no relacionadas con el rendimiento, consérvelas :)
Merlyn Morgan-Graham
1
¿Has probado con la reflexión? Leí en alguna parte que crear instancias por reflexión es más rápido con clases selladas
2010
Que yo sepa no hay ninguno. Sealed está ahí por una razón diferente: bloquear la extensibilidad, que puede ser útil / necesaria en muchos casos. La optimización del rendimiento no era un objetivo aquí.
TomTom
... pero si piensa en el compilador: si su clase está sellada, conoce la dirección de un método que llama en su clase en el momento de la compilación. Si su clase no está sellada, debe resolver el método en tiempo de ejecución porque es posible que deba llamar a una anulación. Ciertamente va a ser insignificante, pero pude ver que habría alguna diferencia.
Dan Puzey
Sí, pero eso no se traduce en beneficios reales, como lo ha señalado el OP. Las diferencias arquitectónicas son / pueden ser mucho más relevantes.
TomTom

Respuestas:

60

El JITter a veces usará 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, así que realmente no puedo describirlas para 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 obtenga de este nivel de optimización debe considerarse 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

Lasse V. Karlsen
fuente
2
el enlace 'divagar' es interesante porque suena a bondad técnica pero en realidad no tiene sentido. Lea los comentarios en el artículo para más información. Resumen: las 3 razones dadas son Control de versiones, rendimiento y seguridad / previsibilidad - [ver siguiente comentario]
Steven A. Lowe
1
[continuación] El control de versiones solo se aplica cuando no hay subclases, duh, pero extiende este argumento a todas las clases y de repente no tienes herencia y adivina qué, ¡el lenguaje ya no está orientado a objetos (sino simplemente basado en objetos)! [ver a continuación]
Steven A. Lowe
3
[continuación] el ejemplo de rendimiento es una broma: optimizar una llamada a método virtual; ¿Por qué una clase sellada tendría un método virtual en primer lugar ya que no se puede subclasificar? Finalmente, el argumento de Seguridad / Previsibilidad es claramente fatuo: 'no se puede usar, por lo que es seguro / predecible'. Jajaja
Steven A. Lowe
16
@Steven A. Lowe: creo que lo que Jeffrey Richter estaba tratando de decir de una manera indirecta es que si dejas la clase sin sellar, debes pensar cómo las clases derivadas pueden usarla, y si no tienes tiempo o inclinación para hacer esto correctamente, luego séllelo, ya que es menos probable que cause cambios importantes en el código de otros en el futuro. Eso no tiene sentido, es buen sentido común.
Greg Beech
66
Una clase sellada puede tener un método virtual, ya que puede derivar de una clase que lo declara. Cuando luego declaras una variable de la clase descendiente sellada y llamas a ese método, el compilador puede emitir una llamada directa a la implementación conocida, ya que sabe que no hay forma de que esto sea diferente de lo que se conoce vtable en tiempo de compilación para esa clase. En cuanto a sellado / no sellado, esa es una discusión diferente, y estoy de acuerdo con las razones para hacer que las clases sellen de forma predeterminada.
Lasse V. Karlsen
143

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

El problema se reduce a los códigos operativos callvs callvirtIL. Calles más rápido que callvirt, y callvirtse usa principalmente cuando no sabes si el objeto ha sido subclasificado. Entonces la gente supone que si sella una clase, todos los códigos de operación cambiarán de calvirtsa callsy serán más rápidos.

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

Las estructuras se usan callporque no pueden subclasificarse y nunca son nulas.

Vea esta pregunta para más información:

Llamada y callvirt

Cameron MacFarland
fuente
55
AFAIK, las circunstancias en las que callse usa son: en la situación new T().Method(), para structmétodos, para llamadas no virtuales a virtualmétodos (como base.Virtual()) o para staticmétodos. En todas partes lo usa callvirt.
Porges
1
Uhh ... me doy cuenta de que esto es viejo, pero esto no está del todo bien ... la gran victoria con las clases selladas es cuando el JIT Optimizer puede alinear la llamada ... en ese caso, la clase sellada puede ser una gran victoria .
Brian Kennedy el
55
¿Por qué esta respuesta es incorrecta? Desde el registro de cambios Mono: "Optimización de la desvirtualización para clases y métodos sellados, mejorando el rendimiento de IronPython 2.0 pystone en un 4%. Otros programas pueden esperar una mejora similar [Rodrigo]". Las clases selladas pueden mejorar el rendimiento, pero como siempre, depende de la situación.
Smilediver
1
@Smilediver Puede mejorar el rendimiento, pero solo si tiene un JIT malo (aunque no tengo idea de lo buenos que son los JIT de .NET hoy en día, solía ser bastante malo en ese sentido). Hotspot, por ejemplo, alineará las llamadas virtuales y desoptimizará más tarde si es necesario; por lo tanto, paga los gastos generales adicionales solo si realmente subclasifica la clase (e incluso entonces no necesariamente).
Voo
1
-1 El JIT no necesariamente tiene que generar el mismo código de máquina para los mismos códigos de operación IL. La comprobación nula y la llamada virtual son pasos ortogonales y separados de callvirt. En el caso de los tipos sellados, el compilador JIT aún puede optimizar parte de callvirt. Lo mismo ocurre cuando el compilador JIT puede garantizar que una referencia no será nula.
derecha el
29

Actualización: a partir de .NET Core 2.0 y .NET Desktop 4.7.1, el CLR ahora admite la desvirtualización. Puede tomar métodos en clases selladas y reemplazar llamadas virtuales con llamadas directas, y también puede hacerlo 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 seguro para desvirtualizar), una clase sellada debería ofrecer algún tipo de beneficio de rendimiento.

Dicho esto, no creo que valga la pena preocuparse a menos que ya haya perfilado el código y haya determinado que estaba en una ruta particularmente caliente que se llamaba 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 descomprimí 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 la versió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 citada a menudo por la cual las personas dicen que el sellado proporciona beneficios de rendimiento es que el compilador sabe que la clase no se anula y, por lo tanto, puede usarla en calllugar de callvirtno tener que buscar virtuales, etc. Como se demostró anteriormente, esto no es cierto.

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

Ejecuté una versión de lanzamiento bajo el depurador 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 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 correr 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 había completado, y ver el desensamblaje del código x86 compilado JIT.

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

Aquí es 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 en el rendimiento entre los métodos de llamada en clases selladas frente a no selladas ... Creo que estoy contento ahora :-)

Orion Edwards
fuente
¿Alguna razón por la que publicaste esta respuesta dos veces en lugar de cerrar la otra como un duplicado? A mí me parecen duplicados válidos, aunque esta no es mi área.
Flexo
1
¿Qué pasaría si los métodos fueran más largos (más de 32 bytes de código IL) para evitar la inserción y ver qué operación de llamada se usaría? Si está en línea, no verá la llamada, por lo que no puede juzgar los efectos.
ygoe
Estoy confundido: ¿por qué hay callvirtcuando estos métodos no son virtuales para empezar?
extraño
@freakish No puedo recordar dónde vi esto, pero leí que el CLR se usaba callvirtpara métodos sellados porque todavía tiene que verificar el objeto antes de invocar la llamada al método, y una vez que lo tiene en cuenta, también puede uso callvirt. Para eliminar callvirty simplemente saltar directamente, tendrían que modificar C # para permitirlo ((string)null).methodCall()como lo hace C ++, o tendrían que demostrar estáticamente que el objeto no era nulo (lo que podrían hacer, pero no se han molestado)
Orion Edwards
1
Props para ir al esfuerzo de excavar al nivel de código de máquina, pero tenga mucho cuidado al hacer declaraciones como 'esto proporciona una prueba sólida de que no puede haber ninguna mejora en el rendimiento'. Lo que ha demostrado es que para un escenario particular no hay diferencia en la salida nativa. Es un punto de datos, y no se puede suponer que se generaliza a todos los escenarios. Para empezar, sus clases no definen ningún método virtual, por lo que no se requieren llamadas virtuales.
Matt Craig
24

Como sé, no hay garantía de beneficio de rendimiento. Pero existe la posibilidad de disminuir la penalización de rendimiento bajo alguna condición específica con un 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 usan 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 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 tuberías estarán inactivas.

Hay un gran obstáculo llamado despacho dinámico que interrumpe esta optimización de 'captación previa'. Puedes entender esto solo como una ramificació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 buscar previamente el siguiente código para ejecutar en este caso porque la siguiente posición del código es desconocida hasta que se resuelva la condición. Esto hace que el peligro provoque que la tubería esté inactiva. Y la penalización de rendimiento por inactividad es enorme en regular.

Algo similar sucede en caso de anulación del 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 solo se puede determinar en tiempo de ejecución. Este también es un caso de despacho dinámico, y, una razón principal de los lenguajes de tipo dinámico son generalmente más lentos que los idiomas de tipo estático.

Algunas CPU (incluidos los recientes chips x86 de Intel) utilizan una técnica llamada ejecución especulativa para utilizar la canalización incluso en la situación. Simplemente busque una ruta de ejecución. Pero la tasa de éxito de esta técnica no es tan alta. Y la falla de la especulación provoca el estancamiento de la tubería, lo que también genera una gran penalización de rendimiento. (Esto es completamente por la implementación de la CPU. Algunas CPU móviles se conocen como no 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 completamente de la implementación del compilador. Algunos compiladores pueden eliminar la posibilidad de envío dinámico al evitar la anulación del método si el método está marcado como sealed. Estúpidos compiladores no pueden. Este es el beneficio de rendimiento de la sealed.


Esta respuesta ( ¿Por qué es más rápido procesar una matriz ordenada que una matriz no clasificada? ) Describe la predicción de ramificación mucho mejor.

eonil
fuente
1
Las CPU de la clase Pentium obtienen previamente el envío indirecto directamente. A veces, la redirección del puntero de función es más rápida que si (no se puede cuestionar) por este motivo.
Joshua
2
Una ventaja de la función no virtual o sellada es que pueden integrarse en más situaciones.
CodesInChaos
4

<off-topic-rant>

Yo detesto clases cerradas. Incluso si los beneficios de rendimiento son asombrosos (lo cual dudo), destruyen el modelo orientado a objetos al evitar 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 subprocesos tendría grandes beneficios. Autores de la clase, si debe sellar sus clases por razones de "rendimiento", proporcione una interfaz como mínimo para que no tengamos que ajustar y reemplazar en todos los lugares en los que necesitemos una función que haya olvidado.

Ejemplo: SafeThread tuvo que ajustar la clase Thread porque Thread está sellado y no hay una interfaz IThread; SafeThread atrapa automáticamente las excepciones no controladas en los subprocesos, algo que falta por completo en la clase Thread. [y no, los eventos de excepción no controlados no recogen excepciones no controladas en subprocesos secundarios].

</off-topic-rant>

Steven A. Lowe
fuente
35
No sello mis clases por razones de rendimiento. Los sello por razones de diseño. Diseñar para la herencia es difícil, y ese esfuerzo se desperdiciará la mayor parte del tiempo. Sin embargo, estoy totalmente de acuerdo en proporcionar interfaces: esa es una solución muy superior para desvelar clases.
Jon Skeet
66
La encapsulación es generalmente una mejor solución que la herencia. Para tomar su ejemplo de subproceso específico, la captura de excepciones de subproceso rompe el Principio de sustitución de Liskov porque ha cambiado el comportamiento documentado de la clase de subproceso, por lo que incluso si pudiera derivar de él, no sería razonable decir que podría usar SafeThread en todas partes podrías usar Thread. En este caso, sería mejor encapsular Thread en otra clase que tenga un comportamiento documentado diferente, lo que puede hacer. A veces las cosas están selladas por tu propio bien.
Greg Beech
1
@ [Greg Beech]: opinión, no hecho: ser capaz de heredar de Thread para arreglar un descuido atroz en su diseño NO es algo malo ;-) Y creo que estás exagerando LSP: la propiedad probable q (x) en este caso es 'una excepción no controlada destruye el programa' que no es una "propiedad deseable" :-)
Steven A. Lowe
1
No, pero he tenido mi parte de código malo donde permití que otros desarrolladores abusen de mis cosas al no sellarlo o al permitir casos de esquina. La mayor parte de mi código hoy en día son afirmaciones y otras cosas relacionadas con el contrato. Y estoy bastante abierto sobre el hecho de que hago esto solo para ser un dolor en el culo.
Turing completo el
2
Ya que estamos haciendo discursos fuera de tema aquí, al igual que odias las clases selladas, detesto las excepciones tragadas. No hay nada peor que cuando algo falla, pero el programa continúa. JavaScript es mi favorito. Realiza un cambio en algún código y de repente hacer clic en un botón no hace absolutamente nada. ¡Excelente! ASP.NET y UpdatePanel es otro; en serio, si mi controlador de botones arroja, es un gran problema y necesita CRASH, ¡así que sé que hay algo que necesita ser arreglado! ¡Un botón que no hace nada es más inútil que un botón que muestra una pantalla de bloqueo!
Roman Starkov
4

Marcar una clase no sealeddebería tener impacto en el rendimiento.

Hay casos en los que cscpodría tener que emitir un callvirtcódigo de operación en lugar de un callcó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 la callvirtque lo haría 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 una vtable, solo llame a la implementación directamente. Para el caso, el JIT puede incluso en línea la función.

Es una especie de apuesta por parte del JIT, porque si una subclase se carga más tarde, el JIT tendrá que tirar 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 victorias de rendimiento).

Jason Orendorff
fuente
3

Las clases selladas deberían proporcionar una mejora del rendimiento. Dado que no se puede derivar una clase sellada, cualquier miembro virtual puede convertirse 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 en el rendimiento a menos que el perfil revelara que es un problema.

Karl Seguin
fuente
Se debe , pero parece que no lo hacen. Si el CLR tuviera el concepto de tipos de referencia no nulos, entonces una clase sellada realmente sería mejor, ya que el compilador podría emitir en calllugar de callvirt... Me encantaría los tipos de referencia no nulos por muchas otras razones también ... suspiro :-(
Orion Edwards
3

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 detección de 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 "sin sellar".

Turing completo
fuente
¿Creo que "omitir" (dejar de lado) debería ser "emitir" (producir)?
user2864740
2

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

Supongo que uno tendrí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

Common Language Infrastructure (CLI) es el estándar ECMA que describe el núcleo de .NET Framework. Shared Source CLI (SSCLI), también conocido como Rotor, es un archivo comprimido del código fuente para una implementación funcional de ECMA CLI y la especificación de lenguaje ECMA C #, tecnologías en el corazón de la arquitectura .NET de Microsoft.

hitec
fuente
La prueba incluyó la creación de una jerarquía de clases, con algunos métodos que estaban haciendo trabajo ficticio (manipulación de cadenas en su mayoría). Algunos de estos métodos fueron virtuales. Se llamaban aquí y allá. Luego llame a estos métodos 100, 10000 y 100000 veces ... y mida el tiempo transcurrido. Luego ejecute estos después de marcar las clases como selladas. y midiendo de nuevo. No hay diferencia en ellos.
Vaibhav
2

las clases selladas serán al menos un poco más rápidas, pero a veces pueden ser mucho más rápidas ... si el JIT Optimizer puede hacer llamadas en línea que de otra forma habrían sido llamadas virtuales. Entonces, donde hay métodos que a menudo 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 heredarlo, así que no voy a dejar que te quemes, suponiendo que fue diseñado para ser así, y no voy a ir para quemarme encerrado 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 LA OPCIÓN MÁS DURADERA ... porque exponer una clase a la derivación te bloquea mucho más que no exponer todo ese. 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.

Brian Kennedy
fuente
El compilador de C # seguirá utilizando callvirt(llamada a método virtual) por ejemplo métodos en clases selladas, porque todavía tiene que hacer una comprobación de objetos nulos en ellos. Con respecto a la alineación, el CLR JIT puede (y lo hace) llamadas de método virtual en línea para clases selladas y no selladas ... así que sí. Lo del rendimiento es un mito.
Orion Edwards
1

Para verlos realmente es necesario analizar el código e compilado JIT (último).

Código C #

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

Código MIL

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

Código Compilado JIT

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

Si bien la creación de los objetos es la misma, las instrucciones ejecutadas para invocar los métodos de la clase sellada y derivada / base son ligeramente diferentes. Después de mover datos a registros o RAM (instrucción mov), la invocación del método sellado, ejecuta una comparación entre dword ptr [ecx], ecx (instrucción cmp) y luego llama al método mientras la clase derivada / base ejecuta directamente el método. .

Según el informe escrito por Torbj¨orn Granlund, latencias de instrucción y rendimiento para procesadores AMD e Intel x86 , la velocidad de las siguientes instrucciones en un Intel Pentium 4 son:

  • mov : tiene 1 ciclo como latencia y el procesador puede soportar 2.5 instrucciones por ciclo de este tipo
  • cmp : tiene 1 ciclo como latencia y el procesador puede soportar 2 instrucciones por ciclo de este tipo

Enlace : https://gmplib.org/~tege/x86-timing.pdf

Esto significa que, idealmente , el tiempo necesario para invocar un método sellado es de 2 ciclos, mientras que el tiempo necesario para invocar un método derivado o de clase base es de 3 ciclos.

La optimización de los compiladores ha marcado la diferencia entre el rendimiento de una clase sellada y no sellada tan baja que estamos hablando de círculos de procesadores y por esta razón son irrelevantes para la mayoría de las aplicaciones.

Ivan Porta
fuente
-10

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

RuslanG
fuente
9
Hay un par de problemas con esto. En primer lugar, no reiniciará el cronómetro entre la primera y la segunda prueba. En segundo lugar, la forma en que llama al método significa que todos los códigos operativos se llamarán, no callvirt, por lo que el tipo no importa.
Cameron MacFarland
1
RuslanG, olvidó llamar a watch.Reset () después de ejecutar la primera prueba. o_O:]
Sí, el código anterior tiene errores. Por otra parte, ¿quién puede presumir de sí mismo para nunca introducir errores en el código? Pero esta respuesta tiene una cosa importante mejor que todas las demás: trata de medir el efecto en cuestión. Y esta respuesta también comparte el código para que todos ustedes inspeccionen y [voten en masa] (me pregunto por qué es necesaria la votación en masa). En contraste con otras respuestas. Eso merece respeto en mi opinión ... También tenga en cuenta que el usuario es novato, por lo que tampoco es un enfoque muy acogedor. Algunas correcciones simples y este código se vuelve útil para todos nosotros. Arreglar + compartir versión fija, si te atreves
Roland Pihlakas
No estoy de acuerdo con @RolandPihlakas. Como dijiste, "quién puede presumir de uno mismo para nunca introducir errores en el código". Respuesta -> No, ninguno. Pero ese no es el punto. El punto es que es una información incorrecta que puede inducir a error a otro nuevo programador. Muchas personas pueden pasar por alto fácilmente que el cronómetro no se restableció. Podrían creer que la información de referencia es verdadera. ¿No es eso más dañino? Alguien que está buscando rápidamente una respuesta puede que ni siquiera lea estos comentarios, sino que vería una respuesta y tal vez lo crea.
Emran Hussain