2011-11-16 15 views
6

me encontré con una diferencia en la velocidad por medio de las siguientes dos estructuras:rendimiento constructor estático y por qué no podemos especificar beforefieldinit

public struct NoStaticCtor 
{ 
    private static int _myValue = 3; 
    public static int GetMyValue() { return _myValue; } 
} 

public struct StaticCtor 
{ 
    private static int _myValue; 
    public static int GetMyValue() { return _myValue; } 
    static StaticCtor() 
    { 
     _myValue = 3; 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     long numTimes = 5000000000; // yup, 5 billion 
     Stopwatch sw = new Stopwatch(); 
     sw.Start(); 
     for (long i = 0; i < numTimes; i++) 
     { 
      NoStaticCtor.GetMyValue(); 
     } 
     sw.Stop(); 
     Console.WriteLine("No static ctor: {0}", sw.Elapsed); 

     sw.Restart(); 
     for (long i = 0; i < numTimes; i++) 
     { 
      StaticCtor.GetMyValue(); 
     } 
     sw.Stop(); 
     Console.WriteLine("with static ctor: {0}", sw.Elapsed); 
    } 
} 

que produce los resultados:

Release (x86), no debugger attached: 
No static ctor: 00:00:05.1111786 
with static ctor: 00:00:09.9502592 

Release (x64), no debugger attached: 
No static ctor: 00:00:03.2595979 
with static ctor: 00:00:14.5922220 

El compilador produce una constructor estático para el NoStaticCtor que es idéntico al declarado explícitamente en StaticCtor. Entiendo que el compilador solo emitirá beforefieldinit cuando un constructor estático no está explícitamente definido.

Producen casi idéntico código il, excepto por una diferencia, declarando la estructura con beforefieldinit, que es donde me siento la diferencia radica ya sé que determina cuando el constructor de tipos se llama, aunque no acabo averiguar por qué hay tanta diferencia. Asume que no es llamando al el constructor de tipos en cada iteración, ya que un constructor de tipos solo se puede llamar una vez.

Así,

1) ¿Por qué la diferencia de tiempo entre la estructura con beforefieldinit y el que no tiene? (I imaginar el JITer está haciendo algo extra en el bucle, sin embargo, no tengo idea de cómo ver la salida del JITer para ver qué.

2) ¿Por qué los diseñadores del compilador a) no hace que todas las estructuras beforefieldinit ser predeterminado yb) no dar a los desarrolladores la capacidad de especificar explícitamente ese comportamiento? Por supuesto, este es asumiendo que no se puede, ya que no he podido encontrar la manera.


Editar:

. I modified the code, que se extiende esencialmente cada bucle por segunda vez, esperando una mejoría, pero no era mucho:

No static ctor: 00:00:03.3342359 
with static ctor: 00:00:14.6139917 
No static ctor: 00:00:03.2229995 
with static ctor: 00:00:12.9524860 
Press any key to continue . . . 

he hecho porque yo, aunque, bueno, tal vez , por improbable que sea, la realidad era JITer llamando al constructor de tipos cada iteración. Me parece que el JITer sabría que el constructor de tipos ya ha sido llamado y no emitirá código para hacer eso cuando se compiló el segundo bucle.

Además de la respuesta de Motti: This code produce mejores resultados, debido a la diferencia en JITing, la JITing de DoSecondLoop no emite el cheque ctor estático, ya que detecta que se hizo anteriormente en DoFirstLoop, causando cada bucle para realizar a la misma velocidad (~ 3 segundos)

+2

Aquí se requiere una cierta perspectiva de los números grandes. La sobrecarga que mediste es * un nanosegundo *. Sí, eso es lo que una instrucción test + jump toma. –

+0

@Hans sé que la sobrecarga es pequeña. Realmente nunca escribiría un código como este usado en producción. Actualmente estoy en un período de "¿cómo funciona el CLR?" Y he estado jugando con cosas inusuales. Realmente estoy tratando de entender por qué el JITer toma las decisiones que toma, en función de los atributos que emite el compilador. Probablemente debería comprar un libro. –

Respuesta

10

La primera vez que se accede a su tipo, el ctor estático (ya sea generado explícita o implícitamente) debe ejecutarse.

Cuando el compilador JIT compila IL a instrucciones nativas comprueba si el ctor estático para ese tipo se ha ejecutado y si no emite código nativo que comprueba (nuevamente) si se ejecutó el ctor estático y si no, lo ejecuta.

El código jitted se almacena en caché para futuras llamadas del mismo método (y esta es la razón por la cual el código tuvo que verificar nuevamente si se ejecutó el ctor estático).

El problema es que si el código jitted que busca el ctor estático está en un bucle, esta prueba se realizará en cada iteración.

Cuando beforefieldinit está presente, el compilador JIT puede optimizar el código, probando la invocación del ctor estático ANTES de ingresar al ciclo. Si no está presente, esta optimización no está permitida.

El compilador C# decide automáticamente cuándo emitir este atributo. Actualmente (C# 4) lo emite solo si el código NO define explícitamente un constructor estático. La idea detrás de esto es que si definió usted mismo un Ctor estático, el tiempo podría ser más importante para usted y el CTST no debería ejecutarse antes de tiempo. Esto puede o no ser cierto, pero no puede cambiar este comportamiento.

Aquí hay un enlace a la parte de mi tutorial .NET en línea que explica esto en detalle: http://motti.me/c1L

Por cierto, no recomendamos el uso de estructuras estáticas ctors porque lo creas o no, ellos no están garantizados ¡ejecutar! Esto no fue parte de la pregunta, así que no daré más detalles, pero vea esto para más detalles si está interesado: http://motti.me/c1I (toco el tema alrededor de las 2:30 en el video).

Espero que esto ayude!

+0

No usar ctors estáticos en las estructuras es un gran consejo. Soy muy consciente de cómo el CLR no siempre puede ejecutarlo. Ahora, al tomar en cuenta el enlace pastebin en mi primera edición, el JITer está poniendo las comprobaciones de ctor estáticas en * ambos * loops que involucran 'StaticCtor.GetMyValue()' en lugar de solo en el primer ciclo for, aunque el JITer sepa se coloca el cheque antes en el código. Esencialmente, en cualquier función que un codificador estático todavía no se haya llamado, pondrá esa verificación antes de que * TODOS * accedan no solo al primero, ¿es correcto? –

+0

Hmm, al probarlo, parece que, como usted dijo, el compilador de JIT toma la decisión de poner la prueba en una base de método por jit. Modifiqué tu código a dos métodos separados por cada llamada (con y sin ctor) y obtuve el refuerzo de rendimiento por segunda vez: http://pastebin.com/WyQWz5ar. No estoy seguro de si esto está documentado o de un detalle de implementación, que podría cambiar en cualquier actualización del compilador JIT. Además, como estoy seguro de que sabe, la implementación del compilador JIT podría ser diferente entre plataformas. –

+0

Sí, lo encontré interesante. Como comenté anteriormente, estoy en una fase en la que solo quiero perder el conocimiento/entender el CLR, y este comportamiento apareció. Me fascina cómo el JIT decide cuándo y qué hacer. ¡Gracias! :) –

Cuestiones relacionadas