【问题标题】:Why the CLR keeps checking static members for type constructor invocation even after the constructor invoked?为什么即使在构造函数调用之后,CLR 也会继续检查静态成员的类型构造函数调用?
【发布时间】:2022-01-22 13:12:53
【问题描述】:

我了解当一个类型声明一个显式静态构造函数时,即时 (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,这种行为会对性能产生影响,因为运行时会执行检查以在精确的时间运行类型初始化程序。

[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 不检查该类型的每个静态方法/实例构造函数以确保先前调用了该类型构造函数,而是检查该类型构造函数是否已被调用(仅一次)?

【问题讨论】:

  • StopwatchDateTime 不是合适的基准测试工具......永远。首先考虑使用 BenchmarkDotNet 解决所有基准问题
  • @TheGeneral - 谢谢。我已经编辑了问题。
  • 能否请您发布您的基准测试结果?
  • @Clemens - 是的,我会的。

标签: c# .net static clr


【解决方案1】:

检查静态构造函数是否已在 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 涵盖的已知 JIT 问题,其中讨论 notes 包含循环的方法默认跳过快速 JIT(请参阅 tiered compilation)。这意味着没有提升静态检查(将其从循环体中取出)的初始编译在程序的生命周期内被锁定。

虽然此性能问题目前尚未解决,但相关的feature request 中提到了更简洁的解决方法;使用 Fody 属性而不是不明显的手动提升调用。

【讨论】:

  • 看来我的结论是错误的; CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE 调用仅在 .cctors 尚未运行时发生),否则将不会调用它。例如:static void Main() {_ = new ExplicitConstructor(); // CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE Console.WriteLine(ExplicitConstructor.MyProperty); // No checks }。对于这种情况(不带beforefieldinit),根据您的回答,似乎有两种方法可以避免这个已知的 JIT 问题: - 手动提升静态检查。 - 使用 Fody 属性。谢谢你。
  • 关闭,但是当 T0 生成被跳过时,JIT 将永远无法提升,(例如,该方法包含一个循环)否则它仍然是稍后重新 JIT 的候选者,当它识别类时已加载并进行了相应优化。
  • 如果您需要在生产代码中缓解这种情况并且您还没有使用 Fody,我建议您使用明确的 Hoist 方法(为清楚起见加上文档注释)以避免引入依赖项。 (Demonstration) 有趣的是,与const 字段交互不会触发类检查。
猜你喜欢
  • 1970-01-01
  • 2020-10-10
  • 2011-05-08
  • 2011-12-07
  • 1970-01-01
  • 2011-09-24
  • 2014-12-07
  • 2012-09-28
  • 1970-01-01
相关资源
最近更新 更多