Esta também não é uma resposta completa, mas tenho algumas idéias.
Acredito que encontrei uma explicação tão boa quanto a encontrada sem que alguém da equipe do .NET JIT responda.
ATUALIZAR
Eu olhei um pouco mais fundo e acredito que encontrei a fonte do problema. Parece ser causado por uma combinação de um erro na lógica de inicialização do tipo JIT e por uma alteração no compilador C # que se baseia na suposição de que o JIT funcione conforme o esperado. Acho que o bug do JIT existia no .NET 4.0, mas foi descoberto pela alteração no compilador do .NET 4.5.
Eu não acho que esse beforefieldinitseja o único problema aqui. Eu acho que é mais simples que isso.
O tipo System.Stringno mscorlib.dll do .NET 4.0 contém um construtor 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
Na versão .NET 4.5 do mscorlib.dll, String.cctor(o construtor estático) está conspicuamente ausente:
..... Nenhum construtor estático :( .....
Nas duas versões, o Stringtipo é adornado beforefieldinit:
.class public auto ansi serializable sealed beforefieldinit System.String
Tentei criar um tipo que seria compilado para IL da mesma forma (para que ele tenha campos estáticos, mas nenhum construtor estático .cctor), mas não consegui. Todos esses tipos têm um .cctormétodo em 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(); }
}
Meu palpite é que duas coisas mudaram entre o .NET 4.0 e o 4.5:
Primeiro: o EE foi alterado para inicializar automaticamente a String.Emptypartir de código não gerenciado. Essa alteração foi provavelmente feita no .NET 4.0.
Segundo: O compilador foi alterado para não emitir um construtor estático para a string, sabendo que isso String.Emptyseria atribuído do lado não gerenciado. Essa alteração parece ter sido feita para o .NET 4.5.
Parece que o EE não atribui o String.Emptysuficiente ao longo de alguns caminhos de otimização. A alteração feita no compilador (ou o que foi alterado para fazer String.cctordesaparecer) esperava que o EE fizesse essa atribuição antes que qualquer código de usuário fosse executado, mas parece que o EE não fez essa atribuição antes String.Emptyé usado em métodos de classes genéricas reificadas do tipo referência.
Por fim, acredito que o bug é indicativo de um problema mais profundo na lógica de inicialização do tipo JIT. Parece que a mudança no compilador é um caso especial System.String, mas duvido que o JIT tenha feito um caso especial aqui System.String.
Original
Primeiro de tudo, WOW O pessoal da BCL ficou muito criativo com algumas otimizações de desempenho. Muitos dos Stringmétodos agora são executados usando um StringBuilderobjeto em cache estático do Thread .
Eu segui esse lead por um tempo, mas StringBuildernão é usado no Trimcaminho do código, então decidi que não poderia ser um problema estático do Thread.
Acho que encontrei uma manifestação estranha do mesmo bug.
Este código falha com uma violação de acesso:
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);
}
}
No entanto, se você descomente //new A<int>(out s);em Mainseguida, o código funciona muito bem. De fato, se Afor reificado com qualquer tipo de referência, o programa falhará, mas se Afor reificado com qualquer tipo de valor, o código não falhará. Além disso, se você comentar o Aconstrutor estático, o código nunca falha. Depois de pesquisar Trime Format, fica claro que o problema está Lengthsendo incorporado e que nessas amostras acima o Stringtipo não foi inicializado. Em particular, dentro do corpo do Aconstrutor de, string.Emptynão está atribuído corretamente, embora dentro do corpo de Main, string.Emptyesteja atribuído corretamente.
É incrível para mim que a inicialização do tipo de Stringalguma forma dependa se é ou não Areificada com um tipo de valor. Minha única teoria é que há algum caminho de código JIT otimizado para inicialização de tipo genérica que é compartilhado entre todos os tipos e que esse caminho faz suposições sobre os tipos de referência BCL ("tipos especiais?") E seu estado. Uma rápida olhada em outras classes BCL com public staticcampos mostra que basicamente todas elas implementam um construtor estático (mesmo aquelas com construtores vazios e sem dados, como System.DBNulle System.Empty. Os tipos de valor BCL com public staticcampos não parecem implementar um construtor estático ( System.IntPtrpor exemplo) Isso parece indicar que o JIT faz algumas suposições sobre a inicialização do tipo de referência BCL.
FYI Aqui está o código JITed para as duas versões:
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
}
O restante do código ( Main) é idêntico entre as duas versões.
EDITAR
Além disso, o IL das duas versões é idêntico, exceto pela chamada para A.ctorin B.Main(), onde o IL da primeira versão contém:
newobj instance void class A`1<object>::.ctor(string&)
versus
... A`1<int32>...
no segundo.
Outra coisa a ser observada é que o código JITed para A<int>.ctor(out string): é o mesmo da versão não genérica.