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 beforefieldinit
seja o único problema aqui. Eu acho que é mais simples que isso.
O tipo System.String
no 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 String
tipo é 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 .cctor
mé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.Empty
partir 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.Empty
seria 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.Empty
suficiente ao longo de alguns caminhos de otimização. A alteração feita no compilador (ou o que foi alterado para fazer String.cctor
desaparecer) 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 String
métodos agora são executados usando um StringBuilder
objeto em cache estático do Thread .
Eu segui esse lead por um tempo, mas StringBuilder
não é usado no Trim
caminho 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 Main
seguida, o código funciona muito bem. De fato, se A
for reificado com qualquer tipo de referência, o programa falhará, mas se A
for reificado com qualquer tipo de valor, o código não falhará. Além disso, se você comentar o A
construtor estático, o código nunca falha. Depois de pesquisar Trim
e Format
, fica claro que o problema está Length
sendo incorporado e que nessas amostras acima o String
tipo não foi inicializado. Em particular, dentro do corpo do A
construtor de, string.Empty
não está atribuído corretamente, embora dentro do corpo de Main
, string.Empty
esteja atribuído corretamente.
É incrível para mim que a inicialização do tipo de String
alguma forma dependa se é ou não A
reificada 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 static
campos mostra que basicamente todas elas implementam um construtor estático (mesmo aquelas com construtores vazios e sem dados, como System.DBNull
e System.Empty
. Os tipos de valor BCL com public static
campos não parecem implementar um construtor estático ( System.IntPtr
por 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.ctor
in 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.