静态构造函数会导致性能开销吗?

最近在dotnetpearls.com上的一篇文章中读到这里的说法,静态ctors采取了大量的性能。

无法理解为什么?

我认为“实质性”在大多数用例中都是夸大其词。

由于beforefieldinit标志的存在/不存在,具有静态构造函数(即使它什么也不做)会影响类型初始化时间。 当你有一个静态构造函数时,有更严格的保证。

对于大多数代码,我建议这没有太大区别 – 但如果你是紧密循环并访问类的静态成员,它可能会。 就个人而言,我不会太担心 – 如果你怀疑它与你的实际应用有关,那么测试它而不是猜测。 微型标记很可能夸大这里的效果。

值得注意的是,当涉及到类型初始化时,.NET 4的行为与以前的版本有些不同 – 所以任何基准测试都应该真正显示不同的版本以便相关。

好吧,我刚刚复制了他的测试。

对于带有DEBUG构建的1000000000次迭代,我得到:

  • 4s为他的静态类与静态构造函数
  • 3.6s相同的类,带有注释掉的静态构造函数
  • 2.9s与非静态类(并在迭代之前创建一个实例)与静态构造函数或不

RELEASE构建相同的function确实突出了一个区别:

  • 带静态构造函数的静态类:4046.875ms
  • 没有静态构造函数的静态类:484.375ms
  • 具有静态构造函数的实例:484.375ms
  • 没有静态构造函数的实例:484.375ms

CLR为静态构造函数的执行提供了非常强大的保证,它承诺只调用它们一次, 然后才能运行类中的任何方法。 当有多个线程使用该类时,实现这种保证是相当棘手的。

看一下SSCLI20的CLR源代码,我看到了相当大的代码块,专门用于提供这种保证。 它维护一个运行静态构造函数的列表,受全局锁保护。 一旦它在该列表中获得一个条目,它就会切换到一个特定于类的锁,以确保没有两个线程可以运行构造函数。 对状态位进行双重检查锁定,指示构造函数已在运行。 许多提供exception保证的不可思议的代码。

好吧,这段代码不是免费的。 将它添加到cctor本身的执行时间,你会看到一些开销。 一如既往,不要让这个抽筋你的风格,这个保证也是一个非常好的,你不想自己提供。 在修复之前进行测量。

我刚做了一个小测试来检查在我的一个类中添加静态构造函数的影响。

我有一个基类,看起来像这样:

public abstract class Base { public abstract Task DoStuffAsync(); } 

问题是,在其中一个实现方法中,该方法什么都不做,所以我可以设置一个预先完成的任务并每次都返回它。

 public sealed class Test1 : Base { readonly Task _emptyTask; public Test1() { TaskCompletionSource source = new TaskCompletionSource(); source.SetResult(null); _emptyTask = source.Task; } public override Task DoStuffAsync() { return _emptyTask; } } 

(其他选项是按需返回任务,但结果总是调用此方法)

这个类的对象经常被创建,通常是循环。 看一下,将_emptyTask设置为静态字段似乎是有益的,因为它对所有方法都是相同的Task

 public sealed class Test2 : Base { static readonly Task _emptyTask; static Test2() { TaskCompletionSource source = new TaskCompletionSource(); source.SetResult(null); _emptyTask = source.Task; } public override Task DoStuffAsync() { return _emptyTask; } } 

然后我记得静态构造函数和性能的“问题”,经过一些研究(我就是这样),我决定做一个小的基准测试:

 Stopwatch sw = new Stopwatch(); List test1list = new List(), test2list = new List(); for (int j = 0; j < 100; j++) { sw.Start(); for (int i = 0; i < 1000000; i++) { Test1 t = new Test1(); if (!t.DoStuffAsync().IsCompleted) throw new Exception(); } sw.Stop(); test1list.Add(sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); for (int i = 0; i < 1000000; i++) { Test2 t = new Test2(); if (!t.DoStuffAsync().IsCompleted) throw new Exception(); } sw.Stop(); test2list.Add(sw.ElapsedMilliseconds); sw.Reset(); GC.Collect(); } Console.WriteLine("Test 1: " + test1list.Average().ToString() + "ms."); Console.WriteLine("Test 2: " + test2list.Average().ToString() + "ms."); 

结果很清楚:

  Test 1: 53.07 ms. Test 2: 5.03 ms. end 

因此,尽管有一个静态构造函数,但好处超过了问题。 总是衡量。