Poniższy przykładowy kod pojawił się naturalnie. Nagle mój kod stał się bardzo nieprzyjemnie brzmiącym FatalExecutionEngineError
wyją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:
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 A
lub zmiana parametru typu z object
na 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
Odpowiedzi:
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
beforefieldinit
to jedyny problem. Myślę, że to prostsze.Typ
System.String
w mscorlib.dll z .NET 4.0 zawiera konstruktor statyczny:W wersji .NET 4.5 mscorlib.dll
String.cctor
(konstruktor statyczny) jest ewidentnie nieobecny:W obu wersjach
String
ozdobionybeforefieldinit
: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ą.cctor
w języku IL metodę: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.Empty
z 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.Empty
zostanie 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.Empty
wystarczająco wcześnie niektórych ścieżek optymalizacji. Zmiana dokonana w kompilatorze (lub cokolwiek zmienionego, abyString.cctor
zniknęł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.Empty
jest 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 przypadekSystem.String
.Oryginalny
Po pierwsze, WOW Ludzie z BCL stali się bardzo kreatywni dzięki pewnym optymalizacjom wydajności. Wiele z tych
String
metod są teraz wykonywane przy użyciu statycznego wątku pamięci podręcznejStringBuilder
obiektu.Podążałem za tym tropem przez jakiś czas, ale
StringBuilder
nie jest używany wTrim
ś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:
Jednakże, jeśli Odkomentuj
//new A<int>(out s);
wMain
to kod działa dobrze. W rzeczywistości, jeśliA
zostanie zreifikowany z dowolnym typem referencyjnym, program zawiedzie, ale jeśliA
zostanie zreifikowany z dowolnym typem wartości, kod nie zawiedzie. Ponadto, jeśliA
wykomentujesz konstruktor statyczny, kod nigdy nie zawiedzie. Po zagłębianiu się wTrim
iFormat
, jest jasne, że problem polega na tym, żeLength
jest on wstawiany i że w tych próbkach powyżejString
typ nie został zainicjowany. W szczególności, wewnątrz korpusuA
„s konstruktora,string.Empty
jest nieprawidłowo przypisana, chociaż wewnątrz korpusuMain
,string.Empty
jest prawidłowe przyporządkowanie.Zaskakujące jest dla mnie, że inicjalizacja typu
String
zależy w jakiś sposób od tego, czyA
jest 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 zpublic static
polami pokazuje, że w zasadzie wszystkie z nich implementują konstruktor statyczny (nawet te z pustymi konstruktorami i bez danych, takie jakSystem.DBNull
iSystem.Empty
. Typy wartości BCL zpublic static
polami nie wydają się implementować konstruktora statycznego (System.IntPtr
na 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)
:A<int32>.ctor(out string)
:Reszta kodu (
Main
) jest identyczna w obu wersjach.EDYTOWAĆ
Ponadto IL z dwóch wersji jest identyczny z wyjątkiem wywołania
A.ctor
inB.Main()
, gdzie IL dla pierwszej wersji zawiera:przeciw
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.źródło
string.Empty
czy""
... :)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?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,
beforefieldinit
jest 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
(lubConsole.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.
źródło
B
nie ma konstruktora statycznego i nie występuje, gdyA
jest reifikowany z typem wartości. Myślę, że jest to trochę bardziej skomplikowane.B
posiadanie statycznego konstruktora nie ma większego znaczenia. PonieważA
ma 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ąpienieA
z typem wartości, może to być drugie przejście środowiska uruchomieniowego przez tworzenie wystąpieniaA
(ś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 .beforefieldinit
optymalizacja 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.A<object>.ctor()
.Obserwacja, ale DotPeek pokazuje zdekompilowany ciąg.
Jeśli zadeklaruję własne w
Empty
ten sam sposób, z wyjątkiem braku atrybutu, nie otrzymam już MDA:źródło
""
rozwiązuje to.