这也不是一个完整的答案,但我有一些想法。
我相信在没有 .NET JIT 团队的任何人回答的情况下,我已经找到了一个很好的解释。
更新
我看的更深了,我相信我找到了问题的根源。它似乎是由 JIT 类型初始化逻辑中的错误和 C# 编译器中的更改引起的,该更改依赖于 JIT 按预期工作的假设。我认为 JIT 错误存在于 .NET 4.0 中,但被 .NET 4.5 的编译器更改发现。
我不认为这beforefieldinit
是这里唯一的问题。我认为它比这更简单。
.NET 4.0 中的 mscorlib.dll 中的类型System.String
包含一个静态构造函数:
.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 的 .NET 4.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(); }
}
我的猜测是 .NET 4.0 和 4.5 之间发生了两件事:
首先:EE 已更改,以便它可以自动String.Empty
从非托管代码初始化。此更改可能是针对 .NET 4.0 进行的。
其次:编译器发生了变化,因此它不会为字符串发出静态构造函数,因为它知道String.Empty
将从非托管端分配。此更改似乎是针对 .NET 4.5 进行的。
似乎 EE在某些优化路径上分配得不够快。String.Empty
对编译器所做的更改(或任何使String.cctor
消失的更改)期望 EE 在执行任何用户代码之前进行此分配,但似乎 EE 在String.Empty
用于引用类型具体化泛型类的方法之前并未进行此分配。
最后,我认为该错误表明 JIT 类型初始化逻辑中存在更深层次的问题。看来编译器中的更改是 的特例System.String
,但我怀疑 JIT 是否在这里为System.String
.
原来的
首先,WOW BCL 人在一些性能优化方面非常有创意。 许多方法String
现在使用 Thread 静态缓存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
and之后Format
,很明显问题在于Length
被内联,并且在上面的这些示例中,String
类型尚未初始化。特别是,在A
的构造函数体内,string.Empty
没有正确分配,尽管在 , 的体内Main
被string.Empty
正确分配。
令我惊讶的是,类型初始化以String
某种方式取决于是否A
用值类型具体化。我唯一的理论是,所有类型共享的通用类型初始化有一些优化的 JIT 代码路径,并且该路径对 BCL 引用类型(“特殊类型?”)及其状态做出了假设。快速浏览其他具有字段的 BCL 类public static
表明,它们基本上都实现了静态构造函数(即使是那些具有空构造函数且没有数据的类,例如System.DBNull
和System.Empty
。具有字段的 BCL 值类型public static
似乎没有实现静态构造函数(System.IntPtr
例如) . 这似乎表明 JIT 对 BCL 引用类型初始化做了一些假设。
仅供参考,这是两个版本的 JITed 代码:
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
) 在两个版本之间是相同的。
编辑
此外,两个版本的 IL 是相同的,除了对A.ctor
in的调用B.Main()
,其中第一个版本的 IL 包含:
newobj instance void class A`1<object>::.ctor(string&)
相对
... A`1<int32>...
在第二。
需要注意的另一件事是 : 的 JITed 代码A<int>.ctor(out string)
与非通用版本中的相同。