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 beforefieldinit
soit le seul problème ici. Je pense que c'est plus simple que ça.
Le type System.String
dans 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 String
type 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 .cctor
mé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.Empty
partir 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.Empty
String.cctor
String.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 String
méthodes sont maintenant effectuées à l' aide d' un cache statique de discussion StringBuilder
objet.
J'ai suivi cette piste pendant un certain temps, mais je ne suis StringBuilder
pas utilisé sur le Trim
chemin 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 Main
alors le code fonctionne très bien. En fait, si A
est réifié avec n'importe quel type de référence, le programme échoue, mais s'il A
est réifié avec n'importe quel type de valeur, le code n'échoue pas. De plus, si vous mettez en commentaire A
le constructeur statique de, le code n'échoue jamais. Après avoir fouillé dans Trim
et Format
, il est clair que le problème est qu'il Length
est en ligne et que dans ces échantillons ci-dessus, le String
type n'a pas été initialisé. En particulier, à l'intérieur du corps du A
constructeur de, string.Empty
n'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 String
dépend en quelque sorte de la réification ou non A
avec 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 static
champs montre que pratiquement toutes implémentent un constructeur statique (même celles avec des constructeurs vides et aucune donnée, comme System.DBNull
et System.Empty
. Les types de valeur BCL avec des public static
champs 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.ctor
in 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.