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 beforefieldinit
sea el único problema aquí. Creo que es más simple que eso.
El tipo System.String
en 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 String
tipo 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 .cctor
mé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.Empty
desde 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.Empty
se asignaría desde el lado no administrado. Este cambio parece haberse realizado para .NET 4.5.
Parece que EE no asigna lo String.Empty
suficientemente 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.cctor
desaparecer) 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.Empty
se 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 String
métodos ahora se realizan utilizando un StringBuilder
objeto en caché estático Thread .
Seguí esa pista por un tiempo, pero StringBuilder
no se usa en la Trim
ruta 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 Main
entonces el código funciona bien. De hecho, si A
se reifica con cualquier tipo de referencia, el programa falla, pero si A
se reifica con cualquier tipo de valor, entonces el código no falla. Además, si comenta A
el constructor estático, el código nunca falla. Después de profundizar en Trim
y Format
, está claro que el problema es que Length
se está alineando, y que en estas muestras anteriores, el String
tipo no se ha inicializado. En particular, dentro del cuerpo del A
constructor, string.Empty
no está asignado correctamente, aunque dentro del cuerpo de Main
, string.Empty
está asignado correctamente.
Es sorprendente para mí que la inicialización de tipo de String
alguna manera dependa de si A
se 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 static
campos muestra que básicamente todas ellas implementan un constructor estático (incluso aquellos con constructores vacíos y sin datos, como System.DBNull
y System.Empty
. Los tipos de valores BCL con public static
campos no parecen implementar un constructor estático ( System.IntPtr
por 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.ctor
in 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.