これも完全な答えではありませんが、私にはいくつかのアイデアがあります。
.NETJITチームの誰かが答えなくてもわかるのと同じくらい良い説明を見つけたと思います。
アップデート
もう少し深く見て、問題の原因を見つけたと思います。これは、JIT型初期化ロジックのバグと、JITが意図したとおりに機能するという前提に依存するC#コンパイラの変更の組み合わせが原因であると思われます。JITのバグは.NET4.0に存在したと思いますが、.NET4.5のコンパイラの変更によって発見されました。
ここでの問題はそれだけではないと思いbeforefieldinit
ます。それよりも簡単だと思います。
System.String
.NET 4.0のmscorlib.dllの型には、静的コンストラクターが含まれています。
.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
mscorlib.dllの.NET4.5バージョンでは、String.cctor
(静的コンストラクター)が著しく存在しません。
.....静的コンストラクターはありません:(....。
どちらのバージョンでも、String
タイプは次のように装飾されていbeforefieldinit
ます。
.class public auto ansi serializable sealed beforefieldinit System.String
同様にILにコンパイルされる型を作成しようとしましたが(静的フィールドはありますが静的コンストラクター.cctor
はありません)、作成できませんでした。これらのタイプはすべて.cctor
、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(); }
}
私の推測では、.NET4.0と4.5の間で2つの変更がありました。
最初:EEはString.Empty
、アンマネージコードから自動的に初期化されるように変更されました。この変更は、おそらく.NET4.0で行われました。
2番目:コンパイラーは、文字列の静的コンストラクターを発行しないように変更されました。これString.Empty
は、アンマネージ側から割り当てられることを認識しています。この変更は、.NET4.5に対して行われたようです。
EEは、いくつかの最適化パスに沿ってすぐに割り当てられないようです。String.Empty
コンパイラに加えられた変更(または消えるように変更されたものString.cctor
)は、EEがユーザーコードを実行する前にこの割り当てを行うことを期待していましたが、EEはString.Empty
、参照型の修正された汎用クラスのメソッドで使用される前にこの割り当てを行わなかったようです。
最後に、このバグは、JITタイプ初期化ロジックのより深い問題を示していると思います。コンパイラの変更はの特殊なケースのようですSystem.String
が、JITがここでの特殊なケースを作成したとは思えませんSystem.String
。
オリジナル
まず第一に、WOW BCLの人々は、いくつかのパフォーマンスの最適化によって非常に創造的になりました。 現在、多くのString
メソッドは、スレッドの静的キャッシュStringBuilder
オブジェクトを使用して実行されています。
私はしばらくそのリードをたどりましたが、コードパスでStringBuilder
は使用されていないため、スレッドの静的な問題ではないと判断しました。Trim
私は同じバグの奇妙な兆候を見つけたと思います。
このコードはアクセス違反で失敗します:
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);
}
}
ただし、コメントを外す//new A<int>(out s);
とMain
、コードは問題なく機能します。実際、A
が任意の参照型で再指定された場合、プログラムは失敗しますが、A
任意の値型で再指定された場合、コードは失敗しません。A
また、の静的コンストラクターをコメントアウトしても、コードが失敗することはありません。Trim
とを掘り下げた後Format
、問題はLength
インライン化されていることであり、上記のサンプルではString
タイプが初期化されていないことは明らかです。特に、A
のコンストラクターの本体内では、string.Empty
が正しく割り当てられていませんが、の本体内では正しく割り当てMain
られていstring.Empty
ます。
どういうわけか型の初期化が値型で具体化String
されているかどうかに依存しているのは私にとって驚くべきことです。A
私の唯一の理論は、すべての型の間で共有される汎用型初期化のための最適化JITコードパスがあり、そのパスはBCL参照型(「特殊型?」)とその状態についての仮定を行うというものです。フィールドを持つ他のBCLクラスをざっと見てみるとpublic static
、基本的にすべてが静的コンストラクターを実装していることがわかります(たとえば、フィールドを持つBCL値型は、静的コンストラクターを実装System.DBNull
していないようです(たとえば)。 。これは、JITがBCL参照型の初期化についていくつかの仮定をしていることを示しているようです。System.Empty
public static
System.IntPtr
参考までに、2つのバージョンのJITコードは次のとおりです。
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
}
残りのコード(Main
)は、2つのバージョン間で同一です。
編集
さらに、2つのバージョンのILは、の呼び出しを除いて同一です。ここA.ctor
でB.Main()
、最初のバージョンのILには次のものが含まれています。
newobj instance void class A`1<object>::.ctor(string&)
対
... A`1<int32>...
第二に。
もう1つの注意点は、:のJITされたコードがA<int>.ctor(out string)
非汎用バージョンと同じであるということです。