Ce n’est pas non plus une réponse complète, mais j’ai quelques idées.
Je crois avoir trouvé une explication aussi bonne que celle que nous trouverons sans que quelqu'un de l'équipe .NET JIT ne réponde.
METTRE À JOUR
J'ai regardé un peu plus profondément et je pense avoir trouvé la source du problème. Cela semble être causé par une combinaison d'un bogue dans la logique d'initialisation de type JIT et d'un changement dans le compilateur C # qui repose sur l'hypothèse que le JIT fonctionne comme prévu. Je pense que le bogue JIT existait dans .NET 4.0, mais a été découvert par la modification du compilateur pour .NET 4.5.
Je ne pense pas que ce beforefieldinitsoit le seul problème ici. Je pense que c'est plus simple que ça.
Le type System.Stringdans mscorlib.dll de .NET 4.0 contient un constructeur statique:
.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
Dans la version .NET 4.5 de mscorlib.dll, String.cctor(le constructeur statique) est manifestement absent:
..... Pas de constructeur statique :( .....
Dans les deux versions, le Stringtype est orné de beforefieldinit:
.class public auto ansi serializable sealed beforefieldinit System.String
J'ai essayé de créer un type qui se compilerait en IL de la même manière (afin qu'il ait des champs statiques mais pas de constructeur statique .cctor), mais je ne pouvais pas le faire. Tous ces types ont une .cctorméthode en 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(); }
}
Je suppose que deux choses ont changé entre .NET 4.0 et 4.5:
Premièrement: l'EE a été modifié pour qu'il s'initialise automatiquement à String.Emptypartir du code non géré. Cette modification a probablement été effectuée pour .NET 4.0.
Deuxièmement: le compilateur a changé de sorte qu'il n'émette pas de constructeur statique pour string, sachant que String.Empty serait affecté du côté non managé. Cette modification semble avoir été effectuée pour .NET 4.5.
Il semble que l'EE n'attribue assez tôt le long des chemins d'optimisation. La modification apportée au compilateur (ou tout ce qui a changé pour faire disparaître) attendait que l'EE fasse cette affectation avant l'exécution de tout code utilisateur, mais il semble que l'EE n'effectue pas cette affectation avantString.EmptyString.cctorString.Empty soit utilisée dans des méthodes de type référence classes génériques réifiées.
Enfin, je pense que le bogue est le signe d'un problème plus profond dans la logique d'initialisation de type JIT. Il semble que le changement dans le compilateur soit un cas particulier pour System.String, mais je doute que le JIT ait fait un cas particulier ici pour System.String.
Original
Tout d'abord, WOW Les gens de la BCL sont devenus très créatifs avec quelques optimisations de performances. Un grand nombre des Stringméthodes sont maintenant effectuées à l' aide d' un cache statique de discussion StringBuilderobjet.
J'ai suivi cette piste pendant un certain temps, mais je ne suis StringBuilderpas utilisé sur le Trimchemin du code, j'ai donc décidé qu'il ne pouvait pas s'agir d'un problème statique de Thread.
Je pense que j'ai trouvé une étrange manifestation du même bug.
Ce code échoue avec une violation d'accès:
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);
}
}
Toutefois, si vous décommentez //new A<int>(out s);dans Mainalors le code fonctionne très bien. En fait, si Aest réifié avec n'importe quel type de référence, le programme échoue, mais s'il Aest réifié avec n'importe quel type de valeur, le code n'échoue pas. De plus, si vous mettez en commentaire Ale constructeur statique de, le code n'échoue jamais. Après avoir fouillé dans Trimet Format, il est clair que le problème est qu'il Lengthest en ligne et que dans ces échantillons ci-dessus, le Stringtype n'a pas été initialisé. En particulier, à l'intérieur du corps du Aconstructeur de, string.Emptyn'est pas correctement assigné, bien qu'à l'intérieur du corps de Main,string.Empty est attribué correctement.
Il est étonnant pour moi que l'initialisation de type Stringdépend en quelque sorte de la réification ou non Aavec un type valeur. Ma seule théorie est qu'il existe un chemin de code JIT optimisant pour l'initialisation de type générique qui est partagé entre tous les types, et que ce chemin fait des hypothèses sur les types de référence BCL ("types spéciaux?") Et leur état. Un rapide coup d'œil sur les autres classes BCL avec des public staticchamps montre que pratiquement toutes implémentent un constructeur statique (même celles avec des constructeurs vides et aucune donnée, comme System.DBNullet System.Empty. Les types de valeur BCL avec des public staticchamps ne semblent pas implémenter un constructeur statique (System.IntPtr par exemple) Cela semble indiquer que le JIT émet des hypothèses sur l'initialisation du type de référence BCL.
FYI Voici le code JITed pour les deux versions:
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
}
Le reste du code ( Main) est identique entre les deux versions.
ÉDITER
De plus, l'IL des deux versions est identique à l'exception de l'appel à A.ctorin B.Main(), où l'IL de la première version contient:
newobj instance void class A`1<object>::.ctor(string&)
contre
... A`1<int32>...
dans la seconde.
Une autre chose à noter est que le code JITed pour A<int>.ctor(out string): est le même que dans la version non générique.