Dies ist auch keine vollständige Antwort, aber ich habe ein paar Ideen.
Ich glaube, ich habe eine so gute Erklärung gefunden, wie wir sie finden werden, ohne dass jemand vom .NET JIT-Team antwortet.
AKTUALISIEREN
Ich habe etwas tiefer geschaut und glaube, die Ursache des Problems gefunden zu haben. Dies scheint auf eine Kombination aus einem Fehler in der JIT-Typinitialisierungslogik und einer Änderung im C # -Compiler zurückzuführen zu sein, die auf der Annahme beruht, dass die JIT wie beabsichtigt funktioniert. Ich denke, der JIT-Fehler war in .NET 4.0 vorhanden, wurde aber durch die Änderung im Compiler für .NET 4.5 aufgedeckt.
Ich denke nicht, dass dies beforefieldinit
hier das einzige Problem ist. Ich denke es ist einfacher als das.
Der Typ System.String
in mscorlib.dll aus .NET 4.0 enthält einen statischen Konstruktor:
.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
In der .NET 4.5-Version von mscorlib.dll String.cctor
fehlt (der statische Konstruktor) auffällig:
..... Kein statischer Konstruktor :( .....
In beiden Versionen ist der String
Typ geschmückt mit beforefieldinit
:
.class public auto ansi serializable sealed beforefieldinit System.String
Ich habe versucht, einen Typ zu erstellen, der ähnlich wie IL kompiliert wird (so dass er statische Felder, aber keinen statischen Konstruktor enthält .cctor
), aber ich konnte es nicht tun. Alle diese Typen haben eine .cctor
Methode in 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(); }
}
Ich vermute, dass sich zwischen .NET 4.0 und 4.5 zwei Dinge geändert haben:
Erstens: Der EE wurde so geändert, dass er automatisch initialisiert wird String.Empty
aus nicht verwaltetem Code . Diese Änderung wurde wahrscheinlich für .NET 4.0 vorgenommen.
Zweitens: Der Compiler wurde so geändert, dass er keinen statischen Konstruktor für Zeichenfolgen ausgibt, da er das weiß String.Empty
dieser von der nicht verwalteten Seite zugewiesen wird. Diese Änderung wurde anscheinend für .NET 4.5 vorgenommen.
Es scheint , dass der EE nicht nicht zuordnen String.Empty
bald genug entlang einiger Optimierungsweg. Die am Compiler vorgenommene Änderung (oder was auch immer geändert wurde, um sie String.cctor
verschwinden zu lassen ) erwartete, dass der EE diese Zuweisung vor String.Empty
der Ausführung eines Benutzercodes vornimmt, aber es scheint, dass der EE diese Zuweisung nicht vornimmt, bevor sie in Methoden mit reifizierten generischen Klassen vom Referenztyp verwendet wird.
Schließlich glaube ich, dass der Fehler auf ein tieferes Problem in der JIT-Typinitialisierungslogik hinweist. Es scheint, dass die Änderung im Compiler ein Sonderfall ist System.String
, aber ich bezweifle, dass die JIT hier einen Sonderfall gemacht hatSystem.String
.
Original
Zunächst einmal WOW Die BCL-Mitarbeiter sind mit einigen Leistungsoptimierungen sehr kreativ geworden. Viele der String
Methoden werden jetzt mit einem statisch zwischengespeicherten Thread- StringBuilder
Objekt ausgeführt.
Ich bin diesem Beispiel eine Weile gefolgt, werde aber StringBuilder
auf dem nicht verwendetTrim
Codepfad verwendet, daher habe ich beschlossen, dass es sich nicht um ein statisches Thread-Problem handeln kann.
Ich glaube, ich habe eine seltsame Manifestation des gleichen Fehlers gefunden.
Dieser Code schlägt mit einer Zugriffsverletzung fehl:
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);
}
}
Wenn jedoch uncomment Sie //new A<int>(out s);
in Main
dann funktioniert der Code nur in Ordnung. Wenn A
es mit einem Referenztyp geändert wird, schlägt das Programm tatsächlich fehl. Wenn A
es jedoch mit einem Werttyp geändert wird, schlägt der Code nicht fehl. Auch wenn Sie den A
statischen Konstruktor auskommentieren, schlägt der Code nie fehl. Nach dem Eingraben in Trim
und Format
ist klar, dass das Problem darin besteht, dass Length
es inline ist und dass in diesen obigen Beispielen der String
Typ nicht initialisiert wurde. Insbesondere wird innerhalb des Körpers des A
Konstruktors string.Empty
nicht korrekt zugewiesen, obwohl innerhalb des Körpers von Main
,string.Empty
ist.
Es ist für mich erstaunlich, dass die Typinitialisierung von String
irgendwie davon abhängt, ob sie A
mit einem Werttyp bestätigt wird oder nicht . Meine einzige Theorie ist, dass es einen optimierenden JIT-Codepfad für die generische Typinitialisierung gibt, der von allen Typen gemeinsam genutzt wird, und dass dieser Pfad Annahmen über BCL-Referenztypen ("spezielle Typen?") Und deren Status macht. Ein kurzer Blick durch andere BCL-Klassen mit public static
Feldern zeigt, dass im Grunde alle einen statischen Konstruktor implementieren (auch solche mit leeren Konstruktoren und ohne Daten wie System.DBNull
und System.Empty
. BCL-Werttypen mit public static
Feldern scheinen keinen statischen Konstruktor zu implementieren (System.IntPtr
zum Beispiel) Dies scheint darauf hinzudeuten, dass die JIT einige Annahmen zur Initialisierung des BCL-Referenztyps trifft.
Zu Ihrer Information Hier ist der JITed-Code für die beiden Versionen:
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
}
Der Rest des Codes ( Main
) ist zwischen den beiden Versionen identisch.
BEARBEITEN
Darüber hinaus ist die IL aus den beiden Versionen identisch, mit Ausnahme des Aufrufs von A.ctor
in B.Main()
, in dem die IL für die erste Version Folgendes enthält:
newobj instance void class A`1<object>::.ctor(string&)
gegen
... A`1<int32>...
in dieser Sekunde.
Eine andere zu beachtende Sache ist, dass der JITed-Code für A<int>.ctor(out string)
: der gleiche ist wie in der nicht generischen Version.