【问题标题】:Static constructor performance and why we can't specify beforefieldinit静态构造函数性能以及为什么我们不能指定 beforefieldinit
【发布时间】:2011-11-16 18:04:18
【问题描述】:

我在使用以下两个结构时遇到了速度差异:

public struct NoStaticCtor
{
    private static int _myValue = 3;
    public static int GetMyValue() { return _myValue; }
}

public struct StaticCtor
{
    private static int _myValue;
    public static int GetMyValue() { return _myValue; }
    static StaticCtor()
    {
        _myValue = 3;
    }
}

class Program
{
    static void Main(string[] args)
    {
        long numTimes = 5000000000; // yup, 5 billion
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (long i = 0; i < numTimes; i++)
        {
            NoStaticCtor.GetMyValue();
        }
        sw.Stop();
        Console.WriteLine("No static ctor: {0}", sw.Elapsed);

        sw.Restart();
        for (long i = 0; i < numTimes; i++)
        {
            StaticCtor.GetMyValue();
        }
        sw.Stop();
        Console.WriteLine("with static ctor: {0}", sw.Elapsed);
    }
}

产生结果:

Release (x86), no debugger attached:
No static ctor: 00:00:05.1111786
with static ctor: 00:00:09.9502592

Release (x64), no debugger attached:
No static ctor: 00:00:03.2595979
with static ctor: 00:00:14.5922220

编译器为NoStaticCtor 生成一个静态构造函数,它与StaticCtor 中显式声明的构造函数相同。我知道编译器只会在未明确定义静态构造函数时发出beforefieldinit

他们产生几乎相同的 il 代码,除了一个区别,用beforefieldinit 声明结构,我觉得区别就在这里,因为我知道它决定了何时调用类型构造函数,虽然我不太明白为什么会有这样的差异。它假定它不是每次迭代都调用类型构造函数,因为类型构造函数只能被调用一次。1

所以,

1) 为什么带有beforefieldinit 的结构和不带beforefieldinit 的结构之间存在时间差? (我想 JITer 在 for 循环中做了一些额外的事情,但是,我不知道如何查看 JITer 的输出以了解什么。

2) 为什么编译器设计者 a) 没有将所有结构 beforefieldinit 设为默认值,并且 b) 没有让开发人员能够明确指定该行为?当然,这假设你不能,因为我一直没能找到方法。


编辑:

1I modified the code,基本上每个循环都运行了第二次,期待改进,但并不多:

No static ctor: 00:00:03.3342359
with static ctor: 00:00:14.6139917
No static ctor: 00:00:03.2229995
with static ctor: 00:00:12.9524860
Press any key to continue . . .

我这样做是因为我虽然,嗯,也许,但不太可能,JITer 实际上在每次迭代时都调用类型构造函数。在我看来,JITer 会知道类型构造函数已被调用,并且在编译第二个循环时不会发出代码来执行此操作。

除了莫蒂的回答: This code 产生更好的结果,由于 JITing 的不同,DoSecondLoop 的 JITing 不会发出静态 ctor 检查,因为它检测到它之前在 DoFirstLoop 中完成,导致每个循环以相同的速度执行。 (约 3 秒)

【问题讨论】:

  • 这里需要对大数有一些看法。您测量的开销是一纳秒。是的,这就是测试+跳转指令所需要的。
  • @Hans 我知道开销很小。我永远不会真正编写像这样用于生产的代码。我目前正处于“CLR 如何工作”的阶段,并且一直在处理不寻常的事情。我真的只是想了解为什么 JITer 根据编译器发出的属性做出决定。我可能应该买一本书。

标签: c# .net performance clr


【解决方案1】:

第一次访问您的类型时,必须执行静态 ctor(无论是显式生成还是隐式生成)。

当 JIT 编译器将 IL 编译为本机指令时,它会检查该类型的静态 ctor 是否已执行,如果没有,则发出本机代码,检查(再次)是否已执行静态 ctor,如果没有,则执行它。

缓存 jit 代码以供将来调用同一方法(这就是代码必须再次检查静态 ctor 是否已执行的原因)。

问题是,如果检查静态 ctor 的 jitted 代码处于循环中,则此测试将在每次迭代中进行。

当出现 beforefieldinit 时,允许 JIT 编译器通过在进入循环之前测试静态 ctor 调用来优化代码。如果不存在,则不允许此优化。

C# 编译器会自动为我们决定何时发出此属性。它目前(C# 4)仅在代码未明确定义静态构造函数时才发出它。它背后的想法是,如果您自己定义了一个静态 ctor,那么时间可能对您来说更重要,并且 ststic ctor 不应该提前执行。这可能是真的,也可能不是,但你不能改变这种行为。

这是我的在线 .NET 教程部分的链接,详细解释了这一点:http://motti.me/c1L

顺便说一句,我不建议在结构上使用静态 ctor,因为不管你信不信,它们不能保证执行!这不是问题的一部分,所以我不会详细说明,但如果您有兴趣,请查看此内容以获得更多详细信息:http://motti.me/c1I(我在视频中大约 2:30 触摸了这个主题)。

我希望这会有所帮助!

【讨论】:

  • 不在结构上使用静态 ctor 是很好的建议。我非常清楚 CLR 可能并不总是执行它。现在,当考虑到我第一次编辑中的 pastebin 链接时,JITer 将静态 ctor 检查放入涉及StaticCtor.GetMyValue()both 循环中,而不是仅在第一个 for 循环中,即使 JITer知道它在代码中较早地放置了检查。本质上,在任何尚未调用静态 ctor 的函数中,它都会在 EVERY 访问之前进行检查,而不仅仅是第一次访问,对吗?
  • 嗯,通过测试似乎正如您所说,JIT 编译器决定是否在每个 jitted 方法的基础上进行测试。我每次调用都将您的代码修改为两个单独的方法(使用 & 不使用 ctor),并第二次获得了性能提升:pastebin.com/WyQWz5ar。我不确定这是记录在案还是实现细节,这可能会随着 JIT 编译器的任何更新而改变。另外,我相信您知道,JIT 编译器的实现可能因平台而异。
  • 是的,我只是觉得它很有趣。正如我上面评论的那样,我正处于一个只想弄乱/理解 CLR 的阶段,并且出现了这种行为。 JIT 如何决定何时以及做什么让我很着迷。谢谢! :)
  • @ChristopherCurrens & MottiShaked,为什么该字段是一个结构体很重要?
  • 它没有。这同样适用于类。
猜你喜欢
  • 2014-05-10
  • 1970-01-01
  • 2015-03-09
  • 1970-01-01
  • 1970-01-01
  • 2013-09-20
  • 1970-01-01
  • 2011-12-08
  • 1970-01-01
相关资源
最近更新 更多