¿Cuál es la causa de este FatalExecutionEngineError en .NET 4.5 beta? [cerrado]

150

El código de muestra a continuación ocurrió naturalmente. De repente, mi código fue una FatalExecutionEngineErrorexcepción muy desagradable . Pasé unos 30 minutos tratando de aislar y minimizar la muestra culpable. Compile esto usando Visual Studio 2012 como una aplicación de consola:

class A<T>
{
    static A() { }

    public A() { string.Format("{0}", string.Empty); }
}

class B
{
    static void Main() { new A<object>(); }
}

Debería producir este error en .NET Framework 4 y 4.5:

Captura de pantalla de FatalExecutionException

¿Es este un error conocido, cuál es la causa y qué puedo hacer para mitigarlo? Mi trabajo actual es no usar string.Empty, pero ¿estoy ladrando el árbol equivocado? Cambiar cualquier cosa sobre ese código hace que funcione como cabría esperar, por ejemplo, eliminar el constructor estático vacío de A, o cambiar el parámetro de tipo de objecta int.

Probé este código en mi computadora portátil y no se quejó. Sin embargo, probé mi aplicación principal y también se estrelló en la computadora portátil. Debo haber destrozado algo al reducir el problema, veré si puedo descubrir qué fue eso.

Mi computadora portátil se bloqueó con el mismo código que el anterior, con Framework 4.0, pero el principal se bloquea incluso con 4.5. Ambos sistemas están utilizando VS'12 con las últimas actualizaciones (¿julio?).

Más información :

  • Código IL (depuración compilada / Cualquier CPU / 4.0 / VS2010 (¿no debería importar el IDE?)): Http://codepad.org/boZDd98E
  • No visto VS 2010 con 4.0. No se bloquea con / sin optimizaciones, CPU de destino diferente, depurador adjunto / no conectado, etc. - Tim Medora
  • Se bloquea en 2010 si uso AnyCPU, está bien en x86. Se bloquea en Visual Studio 2010 SP1, utilizando Platform Target = AnyCPU, pero está bien con Platform Target = x86. Esta máquina también tiene VS2012RC instalado, por lo que 4.5 posiblemente realice un reemplazo en el lugar. Use AnyCPU y TargetPlatform = 3.5, entonces no se bloquea, por lo que parece una regresión en el Framework .
  • No se puede reproducir en x86, x64 o AnyCPU en VS2010 con 4.0. - Fuji
  • Solo sucede para x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC en Win8 RP. Inicialmente No veo este MDA cuando se dirige a .NET 4.5. Cuando se cambió a apuntar a .NET 4.0, apareció la MDA. Luego, después de volver a .NET 4.5, el MDA permanece. - Wayne
Gleno
fuente
Nunca supe que podrías hacer un constructor estático junto con uno público. Diablos, nunca supe que existían los constructores estáticos.
Cole Johnson
Tengo una idea: ¿porque estás cambiando a B de ser una clase estática a una clase con un Main estático?
Cole Johnson
@ChrisSinclair, no lo creo. Quiero decir que probé este código en mi computadora portátil y obtuve los mismos resultados.
Gleno
@ColeJohnson Sí, la IL coincide en todos menos en un lugar obvio. No parece haber ningún error aquí en el compilador de C #.
Michael Graczyk
14
Gracias tanto al póster original por informarlo aquí, como a Michael por su excelente análisis. Mis contrapartes en el CLR intentaron reproducir el error aquí y descubrieron que se reproduce en la versión "Release Candidate" del CLR de 64 bits, pero no en la versión final "Liberado para la fabricación", que tenía una serie de correcciones de errores después de RC. (La versión RTM estará disponible para el público el 15 de agosto de 2012.) Por lo tanto, creen que este es el mismo problema que el que se informó aquí: connect.microsoft.com/VisualStudio/feedback/details/737108/…
Eric Lippert

Respuestas:

114

Esta tampoco es una respuesta completa, pero tengo algunas ideas.

Creo que he encontrado una explicación tan buena como la que encontraremos sin que alguien del equipo de .NET JIT responda.

ACTUALIZAR

Miré un poco más profundo y creo que he encontrado la fuente del problema. Parece ser causado por una combinación de un error en la lógica de inicialización de tipo JIT y un cambio en el compilador de C # que se basa en la suposición de que el JIT funciona según lo previsto. Creo que el error JIT existía en .NET 4.0, pero fue descubierto por el cambio en el compilador para .NET 4.5.

No creo que beforefieldinitsea ​​el único problema aquí. Creo que es más simple que eso.

El tipo System.Stringen mscorlib.dll de .NET 4.0 contiene un constructor estático:

.method private hidebysig specialname rtspecialname static 
    void  .cctor() cil managed
{
  // Code size       11 (0xb)
  .maxstack  8
  IL_0000:  ldstr      ""
  IL_0005:  stsfld     string System.String::Empty
  IL_000a:  ret
} // end of method String::.cctor

En la versión .NET 4.5 de mscorlib.dll, String.cctor(el constructor estático) está notablemente ausente:

..... Sin constructor estático :( .....

En ambas versiones, el Stringtipo está adornado con beforefieldinit:

.class public auto ansi serializable sealed beforefieldinit System.String

Traté de crear un tipo que compilara a IL de manera similar (para que tenga campos estáticos pero sin constructor estático .cctor), pero no pude hacerlo. Todos estos tipos tienen un .cctormétodo en IL:

public class MyString1 {
    public static MyString1 Empty = new MyString1();        
}

public class MyString2 {
    public static MyString2 Empty = new MyString2();

    static MyString2() {}   
}

public class MyString3 {
    public static MyString3 Empty;

    static MyString3() { Empty = new MyString3(); } 
}

Supongo que dos cosas cambiaron entre .NET 4.0 y 4.5:

Primero: el EE se cambió para que se inicializara automáticamente String.Emptydesde el código no administrado. Este cambio probablemente se realizó para .NET 4.0.

Segundo: el compilador cambió para que no emitiera un constructor estático para la cadena, sabiendo que String.Emptyse asignaría desde el lado no administrado. Este cambio parece haberse realizado para .NET 4.5.

Parece que EE no asigna lo String.Emptysuficientemente pronto a lo largo de algunas rutas de optimización. El cambio realizado en el compilador (o lo que sea que haya cambiado para hacer String.cctordesaparecer) esperaba que EE realizara esta asignación antes de que se ejecute cualquier código de usuario, pero parece que EE no realiza esta asignación antes de que String.Emptyse use en métodos de clases genéricas reificadas de tipo de referencia.

Por último, creo que el error es indicativo de un problema más profundo en la lógica de inicialización de tipo JIT. Parece que el cambio en el compilador es un caso especial para System.String, pero dudo que el JIT haya hecho un caso especial aquí para System.String.

Original

En primer lugar, WOW La gente de BCL se ha vuelto muy creativa con algunas optimizaciones de rendimiento. Muchos de los Stringmétodos ahora se realizan utilizando un StringBuilderobjeto en caché estático Thread .

Seguí esa pista por un tiempo, pero StringBuilderno se usa en la Trimruta del código, así que decidí que no podría ser un problema estático de Thread.

Sin embargo, creo que encontré una extraña manifestación del mismo error.

Este código falla con una infracción de acceso:

class A<T>
{
    static A() { }

    public A(out string s) {
        s = string.Empty;
    }
}

class B
{
    static void Main() { 
        string s;
        new A<object>(out s);
        //new A<int>(out s);
        System.Console.WriteLine(s.Length);
    }
}

Sin embargo, si usted elimine el comentario //new A<int>(out s);de Mainentonces el código funciona bien. De hecho, si Ase reifica con cualquier tipo de referencia, el programa falla, pero si Ase reifica con cualquier tipo de valor, entonces el código no falla. Además, si comenta Ael constructor estático, el código nunca falla. Después de profundizar en Trimy Format, está claro que el problema es que Lengthse está alineando, y que en estas muestras anteriores, el Stringtipo no se ha inicializado. En particular, dentro del cuerpo del Aconstructor, string.Emptyno está asignado correctamente, aunque dentro del cuerpo de Main, string.Emptyestá asignado correctamente.

Es sorprendente para mí que la inicialización de tipo de Stringalguna manera dependa de si Ase reifica o no con un tipo de valor. Mi única teoría es que hay una ruta de código de optimización JIT para la inicialización de tipo genérica que se comparte entre todos los tipos, y que esa ruta hace suposiciones sobre los tipos de referencia BCL ("¿tipos especiales?") Y su estado. Un vistazo rápido a otras clases de BCL con public staticcampos muestra que básicamente todas ellas implementan un constructor estático (incluso aquellos con constructores vacíos y sin datos, como System.DBNully System.Empty. Los tipos de valores BCL con public staticcampos no parecen implementar un constructor estático ( System.IntPtrpor ejemplo) Esto parece indicar que el JIT hace algunas suposiciones sobre la inicialización del tipo de referencia BCL.

FYI Aquí está el código JITed para las dos versiones:

A<object>.ctor(out string):

    public A(out string s) {
00000000  push        rbx 
00000001  sub         rsp,20h 
00000005  mov         rbx,rdx 
00000008  lea         rdx,[FFEE38D0h] 
0000000f  mov         rcx,qword ptr [rcx] 
00000012  call        000000005F7AB4A0 
            s = string.Empty;
00000017  mov         rdx,qword ptr [FFEE38D0h] 
0000001e  mov         rcx,rbx 
00000021  call        000000005F661180 
00000026  nop 
00000027  add         rsp,20h 
0000002b  pop         rbx 
0000002c  ret 
    }

A<int32>.ctor(out string):

    public A(out string s) {
00000000  sub         rsp,28h 
00000004  mov         rax,rdx 
            s = string.Empty;
00000007  mov         rdx,12353250h 
00000011  mov         rdx,qword ptr [rdx] 
00000014  mov         rcx,rax 
00000017  call        000000005F691160 
0000001c  nop 
0000001d  add         rsp,28h 
00000021  ret 
    }

El resto del código ( Main) es idéntico entre las dos versiones.

EDITAR

Además, el IL de las dos versiones es idéntico, excepto por la llamada a A.ctorin B.Main(), donde el IL de la primera versión contiene:

newobj     instance void class A`1<object>::.ctor(string&)

versus

... A`1<int32>...

en el segundo.

Otra cosa a tener en cuenta es que el código JITed para A<int>.ctor(out string): es el mismo que en la versión no genérica.

Michael Graczyk
fuente
3
He buscado respuestas a lo largo de un camino muy similar, pero no parece llevar a ninguna parte. Esto parece ser un problema de clase de cadena y, con suerte, no un problema más general. Así que en este momento estoy esperando que alguien (Eric) con el código fuente venga y explique qué salió mal, y si se efectúa algo más. Como un pequeño beneficio, esta discusión ya resolvió el debate sobre si uno debería usar string.Emptyo ""... :)
Gleno
¿La IL entre ellos es la misma?
Cole Johnson
49
Buen análisis! Lo pasaré al equipo de BCL. ¡Gracias!
Eric Lippert
2
@EricLippert y otros: descubrí que el código como typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty);da resultados diferentes en .NET 4.0 frente a .NET 4.5. ¿Está este cambio relacionado con el cambio descrito anteriormente? ¿Cómo puede .NET 4.5 técnicamente ignorarme cambiando un valor de campo? ¿Tal vez debería hacer una nueva pregunta sobre esto?
Jeppe Stig Nielsen
44
@JeppeStigNielsen: Las respuestas a sus preguntas son: "tal vez", "con bastante facilidad, aparentemente" y "este es un sitio de preguntas y respuestas, así que sí, es una buena idea si desea responder mejor a su pregunta que 'tal vez' ".
Eric Lippert
3

Sospecho firmemente que esto es causado por esta optimización (relacionada con BeforeFieldInit) en .NET 4.0.

Si recuerdo correctamente:

Cuando declara un constructor estático explícitamente, beforefieldinitse emite, indicando al tiempo de ejecución que el constructor estático debe ejecutarse antes de que cualquier miembro estático acceda .

Mi conjetura:

Supongo que de alguna manera arruinaron este hecho en el x64 JITer, de modo que cuando se accede a un miembro estático de un tipo diferente desde una clase cuyo propio constructor estático ya se ha ejecutado, de alguna manera omite la ejecución (o se ejecuta en el orden incorrecto) constructor estático y, por lo tanto, provoca un bloqueo. (No obtiene una excepción de puntero nulo, probablemente porque no está inicializado por nulo).

He no ejecutar el código, por lo que esta parte puede estar equivocado - pero si tuviera que hacer otra conjetura, diría que podría ser algo string.Format(o Console.WriteLine, lo que es similar) necesita acceso interno que está causando el accidente, tales como tal vez una clase relacionada con el entorno local que necesita una construcción estática explícita.

Nuevamente, no lo he probado, pero es mi mejor conjetura de los datos.

Siéntase libre de probar mi hipótesis y hágame saber cómo va.

usuario541686
fuente
El error aún ocurre cuando Bno tiene un constructor estático, y no ocurre cuando Ase reifica con un tipo de valor. Creo que es un poco más complicado.
Michael Graczyk
@MichaelGraczyk: Creo que puedo explicar eso (de nuevo, con conjeturas). BTener un constructor estático no importa mucho. Como Atiene un ctor estático, el tiempo de ejecución desordena el orden en que se ejecuta cuando se compara con alguna clase relacionada con la configuración regional en algún otro espacio de nombres. Entonces ese campo aún no se ha inicializado. Sin embargo, si crea Auna instancia con un tipo de valor, entonces podría ser el segundo paso del tiempo de ejecución a través de la creación de instancias A(es probable que el CLR ya lo haya instanciado previamente con un tipo de referencia, como una optimización) para que el orden funcione cuando se ejecuta por segunda vez .
user541686
@MichaelGraczyk: Aunque esta no sea la explicación, creo que estoy bastante convencido de que la beforefieldinitoptimización dada es la causa raíz. Puede ser que parte de la explicación real sea diferente de lo que mencioné, pero la causa raíz es probablemente la misma.
user541686
Miré más en el IL, y creo que estás en algo. No creo que la idea del segundo pase sea relevante aquí, porque el código aún falla si hago arbitrariamente muchas llamadas a A<object>.ctor().
Michael Graczyk
@MichaelGraczyk: Es bueno escucharlo, y gracias por esa prueba. No puedo reproducirlo en mi propia computadora portátil, desafortunadamente. (2010 4.0 x64) ¿Puede verificar si realmente está relacionado con el formato de cadena (es decir, relacionado con la configuración regional)? ¿Qué pasa si quitas esa parte?
user541686
1

Una observación, pero DotPeek muestra la cadena descompilada. Vacía así:

/// <summary>
/// Represents the empty string. This field is read-only.
/// </summary>
/// <filterpriority>1</filterpriority>
[__DynamicallyInvokable]
public static readonly string Empty;

internal sealed class __DynamicallyInvokableAttribute : Attribute
{
  [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
  public __DynamicallyInvokableAttribute()
  {
  }
}

Si declaro el mío de Emptyla misma manera, excepto sin el atributo, ya no obtengo el MDA:

class A<T>
{
    static readonly string Empty;

    static A() { }

    public A()
    {
        string.Format("{0}", Empty);
    }
}
menos código
fuente
¿Y con ese atributo? Ya lo establecimos lo ""resuelve.
Henk Holterman el
Ese atributo "Rendimiento crítico ..." afecta al constructor del atributo en sí, no a los métodos que adorna el atributo.
Michael Graczyk
Es interno. Cuando defino mi propio atributo idéntico, todavía no causa el MDA. No es que lo esperara, si el JITter está buscando ese atributo específico, no encontrará el mío.
lesscode