Anche questa non è una risposta completa, ma ho alcune idee.
Credo di aver trovato una spiegazione altrettanto valida che troveremo senza che qualcuno del team .NET JIT risponda.
AGGIORNARE
Ho guardato un po 'più a fondo e credo di aver trovato la fonte del problema. Sembra essere causato da una combinazione di un bug nella logica di inizializzazione del tipo JIT e da una modifica nel compilatore C # che si basa sul presupposto che JIT funzioni come previsto. Penso che il bug JIT esistesse in .NET 4.0, ma è stato scoperto dalla modifica nel compilatore per .NET 4.5.
Non penso che beforefieldinit
sia l'unico problema qui. Penso che sia più semplice di così.
Il tipo System.String
in mscorlib.dll da .NET 4.0 contiene un costruttore statico:
.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
Nella versione .NET 4.5 di mscorlib.dll, String.cctor
(il costruttore statico) è evidentemente assente:
..... Nessun costruttore statico :( .....
In entrambe le versioni il String
tipo è decorato con beforefieldinit
:
.class public auto ansi serializable sealed beforefieldinit System.String
Ho provato a creare un tipo che si sarebbe compilato in modo simile a IL (in modo che avesse campi statici ma nessun costruttore statico .cctor
), ma non sono riuscito a farlo. Tutti questi tipi hanno un .cctor
metodo 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(); }
}
La mia ipotesi è che due cose sono cambiate tra .NET 4.0 e 4.5:
Primo: l'EE è stato modificato in modo String.Empty
da inizializzarsi automaticamente dal codice non gestito. Questa modifica è stata probabilmente apportata per .NET 4.0.
Secondo: il compilatore è cambiato in modo da non emettere un costruttore statico per stringa, sapendo che String.Empty
sarebbe stato assegnato dal lato non gestito. Questa modifica sembra essere stata apportata per .NET 4.5.
Sembra che EE non si assegni String.Empty
abbastanza presto lungo alcuni percorsi di ottimizzazione. La modifica apportata al compilatore (o qualsiasi altra modifica per far String.cctor
scomparire) prevedeva che EE eseguisse questa assegnazione prima dell'esecuzione di qualsiasi codice utente, ma sembra che EE non esegua tale assegnazione prima che String.Empty
venga utilizzata nei metodi di classi generiche reificate di tipo di riferimento.
Infine, credo che il bug sia indicativo di un problema più profondo nella logica di inizializzazione del tipo JIT. Sembra che il cambiamento nel compilatore sia un caso speciale per System.String
, ma dubito che la JIT abbia fatto un caso speciale qui System.String
.
Originale
Prima di tutto, WOW Le persone BCL sono diventate molto creative con alcune ottimizzazioni delle prestazioni. Molti dei String
metodi vengono ora eseguiti utilizzando un StringBuilder
oggetto memorizzato nella cache statica .
Ho seguito quel comando per un po ', ma StringBuilder
non è usato sul Trim
percorso del codice, quindi ho deciso che non poteva essere un problema statico di Thread.
Penso di aver trovato una strana manifestazione dello stesso bug.
Questo codice ha esito negativo con una violazione di accesso:
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);
}
}
Tuttavia, se si rimuovere il commento //new A<int>(out s);
in Main
poi il codice funziona bene. In effetti, se A
viene reificato con qualsiasi tipo di riferimento, il programma ha esito negativo, ma se A
viene reificato con qualsiasi tipo di valore, il codice non fallisce. Inoltre, se commentate il A
costruttore statico, il codice non fallisce mai. Dopo aver scavato in Trim
e Format
, è chiaro che il problema è che Length
è stato sottolineato e che in questi esempi sopra il String
tipo non è stato inizializzato. In particolare, all'interno del corpo del A
costruttore, string.Empty
non è assegnato correttamente, sebbene all'interno del corpo di Main
, string.Empty
sia assegnato correttamente.
È sorprendente per me che l'inizializzazione del tipo in String
qualche modo dipenda dal fatto che sia o meno A
reificato con un tipo di valore. La mia unica teoria è che esiste un percorso di codice JIT ottimizzante per l'inizializzazione di tipo generico che è condiviso tra tutti i tipi e che quel percorso fa ipotesi sui tipi di riferimento BCL ("tipi speciali?") E sul loro stato. Una rapida occhiata anche se altre classi BCL con public static
campi mostrano che praticamente tutti implementano un costruttore statico (anche quelli con costruttori vuoti e senza dati, come System.DBNull
e System.Empty
. I tipi di valore BCL con public static
campi non sembrano implementare un costruttore statico ( System.IntPtr
, ad esempio) Questo sembra indicare che JIT fa alcune ipotesi sull'inizializzazione del tipo di riferimento BCL.
Cordiali saluti Ecco il codice JITed per le due versioni:
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
}
Il resto del codice ( Main
) è identico tra le due versioni.
MODIFICARE
Inoltre, l'IL delle due versioni è identico ad eccezione della chiamata a A.ctor
in B.Main()
, dove l'IL per la prima versione contiene:
newobj instance void class A`1<object>::.ctor(string&)
contro
... A`1<int32>...
nel secondo.
Un'altra cosa da notare è che il codice JITed per A<int>.ctor(out string)
: è lo stesso della versione non generica.