为什么 CLR 即使在调用构造函数之后仍继续检查静态成员以查找类型构造函数调用?
Why the CLR keeps checking static members for type constructor invocation even after the constructor invoked?
我了解当类型声明显式静态构造函数时,即时 (JIT) 编译器会对该类型的每个静态方法和实例构造函数添加检查,以确保先前调用了静态构造函数.
这个行为我可以想象成下面的代码(如果这个结论有误请指正):
class ExplicitConstructor
{
private static string myVar;
// Force “precise” initialization
static ExplicitConstructor() { myVar = "hello, world";}
/* CLR: if the type constructor didn't invoked
then add a call to the type constructor */
public static string MyProperty
{
get { return myVar; }
}
/* CLR: if the type constructor didn't invoked
then add a call to the type constructor */
public ExplicitConstructor()
{
Console.WriteLine("In instance ctor");
}
}
class ImplicitConstructor
{
private static string myVar = "hello, world";
public static string MyProperty
{
/* CLR: Invoke the type constructor only here */
get { return myVar; }
}
public ImplicitConstructor()
{
Console.WriteLine("In instance ctor");
}
}
根据 performance rules,此行为对性能有影响,因为 运行time 执行检查以在精确时间 运行 类型初始值设定项。
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class BenchmarkExample
{
public const int iteration = Int32.MaxValue - 1;
[Benchmark]
public void BenchExplicitConstructor()
{
for (int i = 0; i < iteration; i++)
{
var temp = ExplicitConstructor.MyProperty;
}
}
[Benchmark]
public void BenchImplicitConstructor()
{
for (int i = 0; i < iteration; i++)
{
var temp = ImplicitConstructor.MyProperty;
}
}
}
Method
Mean
Error
StdDev
Rank
Allocated
BenchImplicitConstructor
982.6 ms
56.64 ms
163.4 ms
1
-
BenchExplicitConstructor
7,361.4 ms
318.19 ms
933.2 ms
2
-
为什么 CLR 没有向类型的每个静态 method/instance 构造函数添加检查以确保先前调用了类型构造函数,而是检查类型构造函数是否已被调用(仅一次) ?
检查 ExplicitConstructor 中是否调用了静态构造函数的成本被夸大了,因为 JIT 未能优化基准方法中的检查 - 如 BenchmarkDotNet DisassemblyDiagnoser
生成的 JITd 程序集所示。
; BenchExplicitConstructor()
push rsi
sub rsp,20
xor esi,esi
M00_L00: ; Hot loop
mov rcx,7FFE299E97C0
mov edx,6
call CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE ; static check
inc esi
cmp esi,7FFFFFFE
jl short M00_L00
add rsp,20
pop rsi
ret
通过确保 ExplicitConstructor 在命中热循环之前已检查(并在本例中已初始化)来帮助解决奇偶校验。
[Benchmark]
public void BenchExplicitConstructor()
{
_ = ExplicitConstructor.MyProperty; // Explicitly check the class
for (int i = 0; i < iteration; i++)
{
var temp = ExplicitConstructor.MyProperty;
}
}
Method
Mean
Error
StdDev
Rank
Code Size
Allocated
BenchImplicitConstructor
672.8 ms
0.23 ms
0.18 ms
1
43 B
3,992 B
BenchExplicitConstructor
673.6 ms
1.19 ms
0.93 ms
1
40 B
384 B
为什么 JIT 不自己做这件事? 在一小段代码的上下文中,没有适当的启发式方法来进行优化。在正常程序的上下文中,这极不可能被遗漏。
更新:
这是 #1327, where the discussion notes that methods containing loops skip quick JITing by default (see tiered compilation 涵盖的已知 JIT 问题。这意味着没有提升静态检查(将其从循环体中移除)的初始编译在程序的生命周期内被锁定。
虽然此性能问题目前尚未解决,但相关的 feature request 中提到了更简洁的解决方法;使用 Fody 属性而不是不明显的手动提升调用。
我了解当类型声明显式静态构造函数时,即时 (JIT) 编译器会对该类型的每个静态方法和实例构造函数添加检查,以确保先前调用了静态构造函数.
这个行为我可以想象成下面的代码(如果这个结论有误请指正):
class ExplicitConstructor
{
private static string myVar;
// Force “precise” initialization
static ExplicitConstructor() { myVar = "hello, world";}
/* CLR: if the type constructor didn't invoked
then add a call to the type constructor */
public static string MyProperty
{
get { return myVar; }
}
/* CLR: if the type constructor didn't invoked
then add a call to the type constructor */
public ExplicitConstructor()
{
Console.WriteLine("In instance ctor");
}
}
class ImplicitConstructor
{
private static string myVar = "hello, world";
public static string MyProperty
{
/* CLR: Invoke the type constructor only here */
get { return myVar; }
}
public ImplicitConstructor()
{
Console.WriteLine("In instance ctor");
}
}
根据 performance rules,此行为对性能有影响,因为 运行time 执行检查以在精确时间 运行 类型初始值设定项。
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class BenchmarkExample
{
public const int iteration = Int32.MaxValue - 1;
[Benchmark]
public void BenchExplicitConstructor()
{
for (int i = 0; i < iteration; i++)
{
var temp = ExplicitConstructor.MyProperty;
}
}
[Benchmark]
public void BenchImplicitConstructor()
{
for (int i = 0; i < iteration; i++)
{
var temp = ImplicitConstructor.MyProperty;
}
}
}
Method | Mean | Error | StdDev | Rank | Allocated |
---|---|---|---|---|---|
BenchImplicitConstructor | 982.6 ms | 56.64 ms | 163.4 ms | 1 | - |
BenchExplicitConstructor | 7,361.4 ms | 318.19 ms | 933.2 ms | 2 | - |
为什么 CLR 没有向类型的每个静态 method/instance 构造函数添加检查以确保先前调用了类型构造函数,而是检查类型构造函数是否已被调用(仅一次) ?
检查 ExplicitConstructor 中是否调用了静态构造函数的成本被夸大了,因为 JIT 未能优化基准方法中的检查 - 如 BenchmarkDotNet DisassemblyDiagnoser
生成的 JITd 程序集所示。
; BenchExplicitConstructor()
push rsi
sub rsp,20
xor esi,esi
M00_L00: ; Hot loop
mov rcx,7FFE299E97C0
mov edx,6
call CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE ; static check
inc esi
cmp esi,7FFFFFFE
jl short M00_L00
add rsp,20
pop rsi
ret
通过确保 ExplicitConstructor 在命中热循环之前已检查(并在本例中已初始化)来帮助解决奇偶校验。
[Benchmark]
public void BenchExplicitConstructor()
{
_ = ExplicitConstructor.MyProperty; // Explicitly check the class
for (int i = 0; i < iteration; i++)
{
var temp = ExplicitConstructor.MyProperty;
}
}
Method | Mean | Error | StdDev | Rank | Code Size | Allocated |
---|---|---|---|---|---|---|
BenchImplicitConstructor | 672.8 ms | 0.23 ms | 0.18 ms | 1 | 43 B | 3,992 B |
BenchExplicitConstructor | 673.6 ms | 1.19 ms | 0.93 ms | 1 | 40 B | 384 B |
为什么 JIT 不自己做这件事? 在一小段代码的上下文中,没有适当的启发式方法来进行优化。在正常程序的上下文中,这极不可能被遗漏。
更新:
这是 #1327, where the discussion notes that methods containing loops skip quick JITing by default (see tiered compilation 涵盖的已知 JIT 问题。这意味着没有提升静态检查(将其从循环体中移除)的初始编译在程序的生命周期内被锁定。
虽然此性能问题目前尚未解决,但相关的 feature request 中提到了更简洁的解决方法;使用 Fody 属性而不是不明显的手动提升调用。