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 :)
.net
optimization
frameworks
performance
Vaibhav
fuente
fuente
Respuestas:
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
fuente
La respuesta es no, las clases selladas no funcionan mejor que las no selladas.
El problema se reduce a los códigos operativos
call
vscallvirt
IL.Call
es más rápido quecallvirt
, ycallvirt
se 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 decalvirts
acalls
y 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,callvirt
se necesita a. Puede evitar esto (sin necesidad de sellar la clase), pero se vuelve un poco inútil.Las estructuras se usan
call
porque no pueden subclasificarse y nunca son nulas.Vea esta pregunta para más información:
Llamada y callvirt
fuente
call
se usa son: en la situaciónnew T().Method()
, parastruct
métodos, para llamadas no virtuales avirtual
métodos (comobase.Virtual()
) o parastatic
métodos. En todas partes lo usacallvirt
.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ó.
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:
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
call
lugar decallvirt
no 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á
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
WriteIt
mé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:
Vs una clase sellada:
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 :-)
fuente
callvirt
cuando estos métodos no son virtuales para empezar?callvirt
para 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 usocallvirt
. Para eliminarcallvirt
y 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)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.
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 lasealed
.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.
fuente
<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>
fuente
Marcar una clase no
sealed
debería tener impacto en el rendimiento.Hay casos en los que
csc
podría tener que emitir uncallvirt
código de operación en lugar de uncall
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 la
callvirt
que lo haríacall
, 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).
fuente
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.
fuente
call
lugar decallvirt
... Me encantaría los tipos de referencia no nulos por muchas otras razones también ... suspiro :-(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".
fuente
@ 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.
fuente
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.
fuente
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.Para verlos realmente es necesario analizar el código e compilado JIT (último).
Código C #
Código MIL
Código Compilado JIT
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:
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.
fuente
Ejecute este código y verá que las clases selladas son 2 veces más rápidas:
salida: clase sellada: 00: 00: 00.1897568 clase no sellada: 00: 00: 00.3826678
fuente