Jaka jest przyczyna tego błędu FatalExecutionEngineError w .NET 4.5 beta? [Zamknięte]

150

Poniższy przykładowy kod pojawił się naturalnie. Nagle mój kod stał się bardzo nieprzyjemnie brzmiącym FatalExecutionEngineErrorwyjątkiem. Spędziłem dobre 30 minut, próbując wyizolować i zminimalizować próbkę sprawcy. Skompiluj to przy użyciu programu Visual Studio 2012 jako aplikacji konsoli:

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

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

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

Powinien spowodować ten błąd w .NET Framework 4 i 4.5:

Zrzut ekranu FatalExecutionException

Czy to znany błąd, jaka jest przyczyna i co mogę zrobić, aby go złagodzić? Moja obecna praca to nie używać string.Empty, ale czy szczekam na niewłaściwe drzewo? Zmiana czegokolwiek w tym kodzie sprawia, że ​​działa on zgodnie z oczekiwaniami - na przykład usunięcie pustego konstruktora statycznego Alub zmiana parametru typu z objectna int.

Wypróbowałem ten kod na moim laptopie i nie narzekał. Jednak wypróbowałem moją główną aplikację i zawiesiła się również na laptopie. Musiałem coś zepsuć, zmniejszając problem, zobaczę, czy uda mi się dowiedzieć, co to było.

Mój laptop zawiesił się z tym samym kodem co powyżej, z frameworkiem 4.0, ale główny wywala nawet z 4.5. Oba systemy używają VS'12 z najnowszymi aktualizacjami (lipiec?).

Więcej informacji :

  • Kod IL (skompilowany Debug / Any CPU / 4.0 / VS2010 (czy nie to IDE powinno mieć znaczenie?)): Http://codepad.org/boZDd98E
  • Nie widziano VS 2010 z 4.0. Brak awarii z optymalizacjami / bez optymalizacji, inny procesor docelowy, debugger podłączony / niepodłączony itp. - Tim Medora
  • Awarie w 2010, jeśli używam AnyCPU, są w porządku w x86. Awarie w programie Visual Studio 2010 z dodatkiem SP1 przy użyciu platformy docelowej = AnyCPU, ale dobrze z platformą docelową = x86. Ta maszyna ma również zainstalowany VS2012RC, więc 4,5 prawdopodobnie robi wymianę na miejscu. Użyj AnyCPU i TargetPlatform = 3.5, a wtedy nie ulegnie awarii, więc wygląda jak regresja w Framework. - colinsmith
  • Nie można odtworzyć na x86, x64 lub AnyCPU w VS2010 z 4.0. - Fuji
  • Dzieje się tylko dla x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC na Win8 RP. Początkowo ten MDA nie jest wyświetlany, gdy jest przeznaczony dla platformy .NET 4.5. Po przejściu na platformę .NET 4.0 pojawił się MDA. Następnie po ponownym przełączeniu na .NET 4.5 MDA pozostaje. - Wayne
Gleno
źródło
Nigdy nie wiedziałem, że można stworzyć statycznego konstruktora razem z publicznym. Heck, nigdy nie wiedziałem, że istnieją statyczne konstruktory.
Cole Johnson
Mam pomysł: ponieważ zmieniasz B z nieco statycznej klasy na klasę ze statycznym Main?
Cole Johnson
@ChrisSinclair, nie sądzę. Mam na myśli, że przetestowałem ten kod na moim laptopie i otrzymałem takie same wyniki.
Gleno
@ColeJohnson Tak, IL pasuje we wszystkich oprócz jednego oczywistego miejsca. Wygląda na to, że w kompilatorze C # nie ma żadnego błędu.
Michael Graczyk
14
Dziękuję zarówno oryginalnemu plakatowi za zgłoszenie go tutaj, jak i Michaelowi za jego doskonałą analizę. Moi odpowiednicy w CLR próbowali odtworzyć błąd tutaj i odkryli, że odtwarza on w wersji „Release Candidate” 64-bitowego CLR, ale nie w ostatecznej wersji „Released To Manufacturing”, która miała wiele poprawek po RC. (Wersja RTM zostanie publicznie udostępniona 15 sierpnia 2012 r.) Dlatego uważają, że jest to ten sam problem, co zgłoszony tutaj: connect.microsoft.com/VisualStudio/feedback/details/737108/ ...
Eric Lippert

Odpowiedzi:

114

To też nie jest pełna odpowiedź, ale mam kilka pomysłów.

Sądzę, że znalazłem równie dobre wyjaśnienie, jakie znajdziemy bez odpowiedzi kogoś z zespołu .NET JIT.

AKTUALIZACJA

Zajrzałem trochę głębiej i wydaje mi się, że znalazłem źródło problemu. Wydaje się, że jest to spowodowane połączeniem błędu w logice inicjalizacji typu JIT i zmianą w kompilatorze C #, która opiera się na założeniu, że JIT działa zgodnie z zamierzeniami. Myślę, że błąd JIT istniał w .NET 4.0, ale został odkryty przez zmianę w kompilatorze dla .NET 4.5.

Nie sądzę, że beforefieldinitto jedyny problem. Myślę, że to prostsze.

Typ System.Stringw mscorlib.dll z .NET 4.0 zawiera konstruktor statyczny:

.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

W wersji .NET 4.5 mscorlib.dll String.cctor(konstruktor statyczny) jest ewidentnie nieobecny:

..... Brak konstruktora statycznego :( .....

W obu wersjach Stringozdobiony beforefieldinit:

.class public auto ansi serializable sealed beforefieldinit System.String

Próbowałem stworzyć typ, który skompilowałby się podobnie do IL (tak, że ma pola statyczne, ale nie ma statycznego konstruktora .cctor), ale nie mogłem tego zrobić. Wszystkie te typy mają .cctorw języku IL metodę:

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(); } 
}

Domyślam się, że między .NET 4.0 a 4.5 zmieniły się dwie rzeczy:

Po pierwsze: zmieniono EE tak, aby był automatycznie inicjowany String.Emptyz niezarządzanego kodu. Ta zmiana została prawdopodobnie wprowadzona dla platformy .NET 4.0.

Po drugie: kompilator zmienił się tak, że nie emitował statycznego konstruktora dla ciągu, wiedząc, że String.Emptyzostanie on przypisany z niezarządzanej strony. Wygląda na to, że ta zmiana została wprowadzona dla platformy .NET 4.5.

Wydaje się, że EE nie wyznacza String.Emptywystarczająco wcześnie niektórych ścieżek optymalizacji. Zmiana dokonana w kompilatorze (lub cokolwiek zmienionego, aby String.cctorzniknęła) oczekiwała, że ​​EE dokona tego przypisania przed wykonaniem jakiegokolwiek kodu użytkownika, ale wydaje się, że EE nie dokonuje tego przypisania wcześniej, String.Emptyjest używane w metodach klas referencyjnych reified klas ogólnych.

Wreszcie uważam, że błąd wskazuje na głębszy problem w logice inicjalizacji typu JIT. Wygląda na to, że zmiana w kompilatorze jest przypadkiem szczególnym System.String, ale wątpię, czy JIT przedstawił tutaj specjalny przypadek System.String.

Oryginalny

Po pierwsze, WOW Ludzie z BCL stali się bardzo kreatywni dzięki pewnym optymalizacjom wydajności. Wiele z tych Stringmetod są teraz wykonywane przy użyciu statycznego wątku pamięci podręcznej StringBuilderobiektu.

Podążałem za tym tropem przez jakiś czas, ale StringBuildernie jest używany w Trimścieżce kodu, więc zdecydowałem, że nie może to być problem statyczny wątku.

Myślę, że znalazłem jednak dziwną manifestację tego samego błędu.

Ten kod kończy się niepowodzeniem z naruszeniem dostępu:

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);
    }
}

Jednakże, jeśli Odkomentuj //new A<int>(out s);w Mainto kod działa dobrze. W rzeczywistości, jeśli Azostanie zreifikowany z dowolnym typem referencyjnym, program zawiedzie, ale jeśli Azostanie zreifikowany z dowolnym typem wartości, kod nie zawiedzie. Ponadto, jeśli Awykomentujesz konstruktor statyczny, kod nigdy nie zawiedzie. Po zagłębianiu się w Trimi Format, jest jasne, że problem polega na tym, że Lengthjest on wstawiany i że w tych próbkach powyżej Stringtyp nie został zainicjowany. W szczególności, wewnątrz korpusu A„s konstruktora, string.Emptyjest nieprawidłowo przypisana, chociaż wewnątrz korpusu Main, string.Emptyjest prawidłowe przyporządkowanie.

Zaskakujące jest dla mnie, że inicjalizacja typu Stringzależy w jakiś sposób od tego, czy Ajest reifikowana typem wartości. Moją jedyną teorią jest to, że istnieje pewna optymalizująca ścieżka kodu JIT dla inicjalizacji typu ogólnego, która jest wspólna dla wszystkich typów, i że ta ścieżka zawiera założenia dotyczące typów referencyjnych BCL („typy specjalne?”) I ich stanu. Szybkie spojrzenie na inne klasy BCL z public staticpolami pokazuje, że w zasadzie wszystkie z nich implementują konstruktor statyczny (nawet te z pustymi konstruktorami i bez danych, takie jak System.DBNulli System.Empty. Typy wartości BCL z public staticpolami nie wydają się implementować konstruktora statycznego ( System.IntPtrna przykład) Wydaje się to wskazywać, że JIT przyjmuje pewne założenia dotyczące inicjalizacji typu referencyjnego BCL.

FYI Oto kod JITed dla dwóch wersji:

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 
    }

Reszta kodu ( Main) jest identyczna w obu wersjach.

EDYTOWAĆ

Ponadto IL z dwóch wersji jest identyczny z wyjątkiem wywołania A.ctorin B.Main(), gdzie IL dla pierwszej wersji zawiera:

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

przeciw

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

w sekundę.

Inną rzeczą, na którą należy zwrócić uwagę, jest to, że kod JITed dla A<int>.ctor(out string): jest taki sam jak w wersji nieogólnej.

Michael Graczyk
źródło
3
Szukałem odpowiedzi na bardzo podobnej ścieżce, ale wydaje się, że nie prowadzi ona do niczego. Wydaje się, że jest to problem z klasą ciągów i miejmy nadzieję, że nie jest to bardziej ogólny problem. Więc teraz czekam, aż ktoś (Eric) z kodem źródłowym przyjdzie i wyjaśni, co poszło nie tak, a jeśli coś jeszcze się stało. Jako mała korzyść ta dyskusja już rozstrzygnęła debatę, czy należy użyć, string.Emptyczy ""... :)
Gleno
Czy IL między nimi jest taka sama?
Cole Johnson,
49
Dobra analiza! Przekażę to zespołowi BCL. Dzięki!
Eric Lippert
2
@EricLippert i inni: Odkryłem, że kod, taki jak, typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty);daje różne wyniki w .NET 4.0 i .NET 4.5. Czy ta zmiana jest związana ze zmianą opisaną powyżej? W jaki sposób .NET 4.5 może technicznie zignorować zmianę wartości pola? Może powinienem zadać nowe pytanie na ten temat?
Jeppe Stig Nielsen
4
@JeppeStigNielsen: Odpowiedzi na twoje pytania to: „może”, „całkiem łatwo, najwyraźniej” i „to jest witryna z pytaniami i odpowiedziami, więc tak, to dobry pomysł, jeśli chcesz lepiej odpowiedzieć na swoje pytanie niż „może” ”.
Eric Lippert
3

Podejrzewam, że jest to spowodowane tą optymalizacją (związaną z BeforeFieldInit) w .NET 4.0.

Jeżeli dobrze pamiętam:

Gdy jawnie deklarujesz konstruktor statyczny, beforefieldinitjest emitowany, informując środowisko uruchomieniowe, że konstruktor statyczny musi zostać uruchomiony przed dostępem do dowolnego statycznego elementu członkowskiego .

Zgaduję że:

Przypuszczam, że jakoś wkręca się ten fakt na JITer x64, tak, że gdy danego różnych Type jest członkiem statyczny jest dostępne z klasą, której własny statyczny konstruktor już biegać, to jakoś pomija uruchomiony (lub wykonującą w niewłaściwej kolejności) konstruktor statyczny - i dlatego powoduje awarię. (Nie otrzymujesz wyjątku wskaźnika o wartości null, prawdopodobnie dlatego, że nie jest on zainicjowany przez wartość null).

Mam nie uruchamiać kod, więc ta część może być źle - ale gdybym miał dokonać innego zgadywać, powiedziałbym, że to może być coś string.Format(lub Console.WriteLine, który jest podobny) wymaga dostępu wewnętrznie, że jest przyczyną awarii, takich jak być może klasa związana z ustawieniami lokalnymi, która wymaga jawnej konstrukcji statycznej.

Ponownie, nie testowałem tego, ale to moje najlepsze przypuszczenie na podstawie danych.

Zapraszam do przetestowania mojej hipotezy i poinformowania mnie, jak poszło.

user541686
źródło
Błąd nadal występuje, gdy Bnie ma konstruktora statycznego i nie występuje, gdy Ajest reifikowany z typem wartości. Myślę, że jest to trochę bardziej skomplikowane.
Michael Graczyk
@MichaelGraczyk: Myślę, że mogę to wyjaśnić (znowu domysłami). Bposiadanie statycznego konstruktora nie ma większego znaczenia. Ponieważ Ama statyczny ctor, środowisko wykonawcze zmienia kolejność, w jakiej jest uruchamiane, w porównaniu z pewną klasą związaną z ustawieniami lokalnymi w innej przestrzeni nazw. Więc to pole nie zostało jeszcze zainicjowane. Jeśli jednak utworzysz wystąpienie Az typem wartości, może to być drugie przejście środowiska uruchomieniowego przez tworzenie wystąpienia A(środowisko CLR prawdopodobnie już wstępnie utworzyło wystąpienie z typem referencyjnym jako optymalizacją), więc kolejność działa, gdy jest uruchamiana po raz drugi .
user541686
@MichaelGraczyk: Nawet jeśli to nie jest do końca wyjaśnienie - myślę, że jestem przekonany, że to właśnie ta beforefieldinitoptymalizacja jest przyczyną źródłową . Być może niektóre z rzeczywistych wyjaśnień różnią się od tego, o czym wspomniałem, ale główną przyczyną jest prawdopodobnie to samo.
user541686
Zaglądałem więcej do IL i myślę, że coś wpadłeś. Nie sądzę, aby pomysł drugiego przejścia był tutaj istotny, ponieważ kod nadal zawodzi, jeśli wykonam dowolną liczbę wywołań A<object>.ctor().
Michael Graczyk
@MichaelGraczyk: Dobrze to słyszeć i dzięki za ten test. Nie mogę niestety odtworzyć tego na własnym laptopie. (2010 4.0 x64) Czy możesz sprawdzić, czy rzeczywiście jest to związane z formatowaniem napisów (tj. Związane z ustawieniami regionalnymi)? Co się stanie, jeśli usuniesz tę część?
user541686
1

Obserwacja, ale DotPeek pokazuje zdekompilowany ciąg.

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

Jeśli zadeklaruję własne w Emptyten sam sposób, z wyjątkiem braku atrybutu, nie otrzymam już MDA:

class A<T>
{
    static readonly string Empty;

    static A() { }

    public A()
    {
        string.Format("{0}", Empty);
    }
}
lesscode
źródło
A z tym atrybutem? Ustaliliśmy już, ""rozwiązuje to.
Henk Holterman,
Atrybut „Krytyczny dla wydajności ...” wpływa na sam konstruktor atrybutu, a nie na metody, które zdobi atrybut.
Michael Graczyk,
To jest wewnętrzne. Kiedy definiuję swój własny identyczny atrybut, nadal nie powoduje to MDA. Nie to, żebym tego oczekiwał - jeśli JITter szuka tego konkretnego atrybutu, nie znajdzie mojego.
lesscode