为什么 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 属性而不是不明显的手动提升调用。