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.