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:
.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 String
ozdobiony 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ą .cctor
w 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.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, aby String.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 przypadek System.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ęcznej StringBuilder
obiektu.
Podążałem za tym tropem przez jakiś czas, ale StringBuilder
nie 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 Main
to kod działa dobrze. W rzeczywistości, jeśli A
zostanie zreifikowany z dowolnym typem referencyjnym, program zawiedzie, ale jeśli A
zostanie zreifikowany z dowolnym typem wartości, kod nie zawiedzie. Ponadto, jeśli A
wykomentujesz konstruktor statyczny, kod nigdy nie zawiedzie. Po zagłębianiu się w Trim
i Format
, jest jasne, że problem polega na tym, że Length
jest on wstawiany i że w tych próbkach powyżej String
typ nie został zainicjowany. W szczególności, wewnątrz korpusu A
„s konstruktora, string.Empty
jest nieprawidłowo przypisana, chociaż wewnątrz korpusu Main
, string.Empty
jest prawidłowe przyporządkowanie.
Zaskakujące jest dla mnie, że inicjalizacja typu String
zależy w jakiś sposób od tego, czy A
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 z public static
polami pokazuje, że w zasadzie wszystkie z nich implementują konstruktor statyczny (nawet te z pustymi konstruktorami i bez danych, takie jak System.DBNull
i System.Empty
. Typy wartości BCL z public 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)
:
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.ctor
in 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.