Qual é a causa desse FatalExecutionEngineError no .NET 4.5 beta? [fechadas]

150

O código de exemplo abaixo ocorreu naturalmente. De repente, meu código provocou uma FatalExecutionEngineErrorexceção muito desagradável . Passei bons 30 minutos tentando isolar e minimizar a amostra culpada. Compile isso usando o Visual Studio 2012 como um aplicativo de console:

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

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

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

Deve produzir esse erro no .NET framework 4 e 4.5:

Captura de tela de FatalExecutionException

Esse erro é conhecido, qual é a causa e o que posso fazer para atenuá-lo? Meu trabalho atual é não usar string.Empty, mas estou latindo na árvore errada? Alterar qualquer coisa sobre esse código faz com que funcione como você esperaria - por exemplo, removendo o construtor estático vazio de Aou alterando o parâmetro de tipo de objectpara int.

Eu tentei esse código no meu laptop e ele não se queixou. No entanto, eu tentei o meu aplicativo principal e ele travou no laptop também. Eu devo ter destruído alguma coisa ao reduzir o problema, vou ver se consigo descobrir o que era isso.

Meu laptop travou com o mesmo código acima, com o framework 4.0, mas o principal travou mesmo com o 4.5. Ambos os sistemas estão usando o VS'12 com as atualizações mais recentes (julho?).

Mais informações :

  • Código IL (compilação de depuração / qualquer CPU / 4.0 / VS2010 (não que o IDE importe?)): Http://codepad.org/boZDd98E
  • Não visto VS 2010 com 4.0. Não travar com / sem otimizações, CPU de destino diferente, depurador conectado / não conectado, etc. - Tim Medora
  • Falha em 2010, se eu usar o AnyCPU, está bem em x86. Falha no Visual Studio 2010 SP1, usando o Target Platform = AnyCPU, mas é bom com o Target Platform = x86. Esta máquina também possui o VS2012RC instalado e, portanto, o 4.5 está fazendo uma substituição no local. Use AnyCPU e TargetPlatform = 3.5 para não travar, por isso parece uma regressão no Framework. - colinsmith
  • Não é possível reproduzir em x86, x64 ou AnyCPU no VS2010 com 4.0. - Fuji
  • Só acontece para x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC no Win8 RP. Inicialmente Não está vendo este MDA ao direcionar o .NET 4.5. Quando passou para o .NET 4.0, o MDA apareceu. Depois de voltar ao .NET 4.5, o MDA permanece. - Wayne
Gleno
fonte
Eu nunca soube que você poderia fazer um construtor estático junto com um público. Heck, eu nunca soube que existiam construtores estáticos.
Cole Johnson
Eu tenho uma idéia: porque você está mudando B de ser uma classe estática para apenas uma classe com uma estática Main?
Cole Johnson
@ ChrisSinclair, acho que não. Quero dizer, testei esse código no meu laptop e obtive os mesmos resultados.
Gleno
@ColeJohnson Sim, o IL corresponde em todos, exceto no único lugar óbvio. Não parece haver nenhum erro aqui no compilador c #.
Michael Graczyk
14
Agradecemos ao pôster original por reportá-lo aqui e a Michael por sua excelente análise. Minhas contrapartes no CLR tentaram reproduzir o bug aqui e descobriram que ele é reproduzido na versão "Release Candidate" do CLR de 64 bits, mas não na versão final "Released To Manufacturing", que teve várias correções de erros pós- RC. (A versão RTM estará disponível ao público em 15 de agosto de 2012.) Portanto, eles acreditam que esse seja o mesmo problema relatado aqui: connect.microsoft.com/VisualStudio/feedback/details/737108/…
Eric Lippert

Respostas:

114

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.

Michael Graczyk
fonte
3
Procurei respostas em um caminho muito semelhante, mas parece não levar a lugar algum. Parece ser um problema de classe de string e, esperançosamente, não é um problema mais geral. Então, agora, estou esperando alguém (Eric) com o código-fonte aparecer e explicar o que deu errado, e se alguma outra coisa for realizada. Como um pequeno benefício essa discussão já resolvido o debate se se deve usar string.Emptyou ""... :)
Gleno
A IL entre eles é a mesma?
Cole Johnson
49
Boa análise! Vou passar para a equipe da BCL. Obrigado!
22712 Eric Clippert
2
@ EricLippert e outros: eu descobri que o código como typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty);dá resultados diferentes no .NET 4.0 versus .NET 4.5. Essa alteração está relacionada à alteração descrita acima? Como o .NET 4.5 tecnicamente me ignora alterando um valor de campo? Talvez eu deva fazer uma nova pergunta sobre isso?
Jeppe Stig Nielsen
4
@JeppeStigNielsen: As respostas para suas perguntas são: "talvez", "facilmente" e "este é um site de perguntas e respostas, então sim, é uma boa idéia se você deseja uma resposta melhor à sua pergunta do que 'talvez' ".
Eric Lippert
3

Eu suspeito fortemente que isso seja causado por essa otimização (relacionada a BeforeFieldInit) no .NET 4.0.

Se eu me lembro bem:

Quando você declara explicitamente um construtor estático, beforefieldinité emitido, informando ao tempo de execução que o construtor estático deve ser executado antes de qualquer membro estático acessar .

Meu palpite:

Eu acho que eles de alguma forma estragaram esse fato no x64 JITer, de modo que quando um membro estático de um tipo diferente é acessado de uma classe cujo próprio construtor estático já foi executado, ele de alguma forma ignora a execução (ou executa na ordem errada) o construtor estático - e, portanto, causa uma falha. (Você não recebe uma exceção de ponteiro nulo, provavelmente porque não foi inicializado com nulo.)

Como não executei seu código, esta parte pode estar errada - mas se eu precisasse adivinhar, diria que pode ser que algo string.Format(ou Console.WriteLinesemelhante) precise acessar internamente e esteja causando o travamento, como talvez uma classe relacionada ao código do idioma que precise de construção estática explícita.

Novamente, não testei, mas é o meu melhor palpite sobre os dados.

Sinta-se à vontade para testar minha hipótese e me informar como ela é.

user541686
fonte
O bug ainda ocorre quando Bnão possui um construtor estático e não ocorre quando Aé reificado com um tipo de valor. Eu acho que é um pouco mais complicado.
22812 Michael Graczyk
@ MichaelGraczyk: Acho que posso explicar isso (novamente, com palpites). Bter um construtor estático não importa muito. Como Apossui um controlador estático, o tempo de execução altera a ordem em que é executado quando comparado a alguma classe relacionada ao código do idioma em outro espaço de nome. Portanto, esse campo ainda não foi inicializado. No entanto, se você instanciar Acom um tipo de valor, pode ser a segunda passagem do tempo de execução pela instanciação A(o CLR provavelmente já o pré-instanciou com um tipo de referência, como uma otimização), para que o pedido funcione quando for executado pela segunda vez .
user541686
@ MichaelGraczyk: Mesmo que essa não seja a explicação, acho que estou convencido de que a beforefieldinitotimização fornecida é a causa raiz. Pode ser que algumas das explicações reais sejam diferentes das mencionadas, mas a causa raiz provavelmente é a mesma coisa.
precisa saber é o seguinte
Eu olhei mais para a IL e acho que você gosta de algo. Não acho que a idéia da segunda passagem seja relevante aqui, porque o código ainda falha se eu fizer arbitrariamente muitas chamadas para A<object>.ctor().
Michael Graczyk
@ MichaelGraczyk: É bom ouvir isso, e obrigado por esse teste. Infelizmente, não posso reproduzi-lo no meu laptop. (2010 4.0 x64) Você pode verificar se está realmente relacionado à formatação de strings (ou seja, relacionadas à localidade)? O que acontece se você remover essa parte?
user541686
1

Uma observação, mas o DotPeek mostra a string descompilada.

/// <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()
  {
  }
}

Se eu declarar o meu Emptyda mesma maneira, exceto sem o atributo, não receberei mais o MDA:

class A<T>
{
    static readonly string Empty;

    static A() { }

    public A()
    {
        string.Format("{0}", Empty);
    }
}
lesscode
fonte
E com esse atributo? Nós já estabelecidos ""resolve isso.
Henk Holterman
Esse atributo "Desempenho crítico ..." afeta o próprio construtor Attribute, não os métodos que o atributo adorna.
Michael Graczyk
É interno. Quando defino meu próprio atributo idêntico, ele ainda não causa o MDA. Não que eu esperasse - se o JITter estiver procurando por esse atributo específico, ele não encontrará o meu.
lesscode 9/08/12