¿Por qué el compilador de C# emite una instrucción callvirt para una llamada al método GetType ()?


Tengo curiosidad por saber por qué está sucediendo esto. Por favor, lea el ejemplo de código a continuación y el IL correspondiente que se emitió en los comentarios debajo de cada sección:

using System;

class Program
{
    static void Main()
    {
        Object o = new Object();
        o.GetType();

        // L_0001: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0006: stloc.0 
        // L_0007: ldloc.0 
        // L_0008: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()

        new Object().GetType();

        // L_000e: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0013: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
    }
}

¿Por qué el compilador emitió un callvirt para la primera sección pero un call para la segunda sección? ¿Hay alguna razón para que el compilador emita alguna vez una instrucción callvirt para un método no virtual? Y si hay casos en los que el compilador emitirá un callvirt para un método no virtual, esto crea problemas para tipo de seguridad?

Author: Andrew Hare, 2009-05-10

5 answers

Sólo jugaba a lo seguro.

Técnicamente el compilador de C # no siempre usa callvirt

Para métodos estáticos y métodos definidos en tipos de valor, utiliza call. La mayoría se proporciona a través de la instrucción IL callvirt.

La diferencia que hizo girar el voto entre los dos es el hecho de que call asume que el "objeto que se usa para hacer la llamada" no es nulo. callvirt por otro lado comprueba not null y lanza una excepción NullReferenceException si es necesario.

  • Para métodos estáticos, el objeto es un objeto de tipo y no puede ser null. Lo mismo ocurre con los tipos de valor. Por lo tanto call se utiliza para ellos-mejor rendimiento.
  • Para los demás, los diseñadores del lenguaje decidieron ir con callvirt para que el compilador JIT verifique que el objeto que se utiliza para hacer la llamada no sea null. Incluso para métodos de instancia no virtuales.. valoraban la seguridad por encima del rendimiento.

Ver también: Jeff Richter hace un mejor trabajo en esto - en su 'Diseño Capítulo de tipos en CLR vía C # 2nd Ed

 20
Author: Gishu,
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-05-10 17:32:20

Ver este antiguo post del blog de Eric Gunnerson.

Aquí está el texto del post:

¿Por qué C# siempre usa callvirt?

Esta pregunta surgió en un alias interno de C#, y pensé que la respuesta sería de interés general. Eso es asumiendo que la respuesta es correcta - ha pasado bastante tiempo.

El lenguaje.NET IL proporciona tanto una llamada como una instrucción callvirt, con la callvirt siendo utilizada para llamar a funciones virtuales. Pero si miras a través de la código que genera C#, verá que genera un "callvirt" incluso en los casos en que no hay una función virtual involucrada. ¿Por qué hace eso?

Revisé las notas de diseño del lenguaje que tengo, y dicen claramente que decidimos usar callvirt el 13/12/1999. Desafortunadamente, no capturan nuestra lógica para hacer eso, así que voy a tener que ir de mi memoria.

Habíamos recibido un informe de alguien (probablemente uno de los grupos. NET que usan C # todavía no se llamaba C# en ese momento)) que había escrito código que llamaba a un método en un puntero nulo, pero no obtuvieron una excepción porque el método no accedía a ningún campo (es decir, "esto" era nulo, pero nada en el método lo usaba). Ese método luego llamó a otro método que usó el este punto y lanzó una excepción, y un poco de rascarse la cabeza siguió. Después de averiguarlo, nos enviaron una nota al respecto.

Pensamos que poder llamar a un método en una instancia nula era un un poco raro. Peter Golde hizo algunas pruebas para ver cuál era el impacto perf de usar siempre callvirt, y fue lo suficientemente pequeño como para que decidiéramos hacer el cambio.

 27
Author: Dustin Campbell,
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-05-10 17:11:27

Como un (quizás-)interesante aparte... {[0] } es inusual en que no lo es virtual - esto conduce a algunas cosas muy, muy extrañas.

(marcado como wiki, ya que está un poco fuera de tema para la pregunta real)

 3
Author: Marc Gravell,
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:48:38

El compilador no conoce el tipo real de o en la primera expresión, pero sí conoce el tipo real en la segunda expresión. Parece que solo está mirando una declaración a la vez.

Esto está bien, porque C# depende en gran medida del JIT para la optimización. Es muy probable que en un caso tan simple ambas llamadas se conviertan en llamadas de instancia en tiempo de ejecución.

No creo que callvirt se emita alguna vez para métodos no virtuales, pero incluso si lo fuera, no sería un problema porque el método nunca sería anulado (por razones obvias).

 1
Author: zildjohn01,
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-05-10 17:08:42

Me atrevería a suponer que es porque la primera asigna a una variable, que potencialmente podría contener una instancia de otro tipo que podría haber anulado GetType (aunque podemos ver que no lo hace); la segunda nunca podría ser otra cosa que Object.

 0
Author: James Gregory,
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-05-10 17:10:07