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 beforefieldinitsia l'unico problema qui. Penso che sia più semplice di così.
Il tipo System.Stringin 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 Stringtipo è 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 .cctormetodo 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.Emptyda 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.Emptysarebbe stato assegnato dal lato non gestito. Questa modifica sembra essere stata apportata per .NET 4.5.
Sembra che EE non si assegni String.Emptyabbastanza presto lungo alcuni percorsi di ottimizzazione. La modifica apportata al compilatore (o qualsiasi altra modifica per far String.cctorscomparire) prevedeva che EE eseguisse questa assegnazione prima dell'esecuzione di qualsiasi codice utente, ma sembra che EE non esegua tale assegnazione prima che String.Emptyvenga 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 Stringmetodi vengono ora eseguiti utilizzando un StringBuilderoggetto memorizzato nella cache statica .
Ho seguito quel comando per un po ', ma StringBuildernon è usato sul Trimpercorso 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 Mainpoi il codice funziona bene. In effetti, se Aviene reificato con qualsiasi tipo di riferimento, il programma ha esito negativo, ma se Aviene reificato con qualsiasi tipo di valore, il codice non fallisce. Inoltre, se commentate il Acostruttore statico, il codice non fallisce mai. Dopo aver scavato in Trime Format, è chiaro che il problema è che Lengthè stato sottolineato e che in questi esempi sopra il Stringtipo non è stato inizializzato. In particolare, all'interno del corpo del Acostruttore, string.Emptynon è assegnato correttamente, sebbene all'interno del corpo di Main, string.Emptysia assegnato correttamente.
È sorprendente per me che l'inizializzazione del tipo in Stringqualche modo dipenda dal fatto che sia o meno Areificato 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 staticcampi mostrano che praticamente tutti implementano un costruttore statico (anche quelli con costruttori vuoti e senza dati, come System.DBNulle System.Empty. I tipi di valore BCL con public staticcampi 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.ctorin 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.