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

18

我最近在dotnetpearls.com上读了一篇文章,在这里说静态构造函数会导致性能受到相当大的影响。

不明白为什么会这样?

4个回答

21

我认为"substantial"在大多数情况下都有些夸张。

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

对于大多数代码,我建议这并没有太大区别 - 但如果你正在紧密循环和访问类的静态成员,那就可能会有所不同。个人认为不必太过担心 - 如果你怀疑它在你的真实应用程序中有关,那么测试它而不是猜测。在这里,微基准测试很可能会夸大效果。

值得注意的是,.NET 4 在类型初始化方面与之前的版本有所不同 - 因此,任何基准测试应该真正显示不同的版本才能具有相关性。


11

我刚刚复制了他的测试。

在使用DEBUG版本进行1000000000次迭代后,我得到如下结果:

  • 使用带有静态构造函数的静态类:4秒钟
  • 使用已注释掉静态构造函数的同一类:3.6秒钟
  • 将类设置为非静态(并在迭代之前创建一个实例),无论是否带有静态构造函数,都需要2.9秒钟

同样是使用RELEASE版本,则会出现差异:

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

你用的是哪个版本的.NET呢?我猜这是一个非调试优化构建,在调试器外运行的? - Jon Skeet
3.5 SP1 - 嗯,事后看来可能是以调试构建的方式构建了这个...让我再试一次,用一个适当的发布构建。 - Paolo
没错 - 这并不是很令人惊讶。现在如果您已经安装了.NET 4,请也尝试一下...但当然要记住,这个微基准测试并不是一个真实的测试。 - Jon Skeet
很遗憾,这台机器上没有.NET 4,我将把它留给读者作为练习 :) - Paolo
在我的时间测试中,.net4似乎没有太大的区别,甚至有时会偏向于静态构造函数! - ohmusama

7
CLR提供了一个相当强的保证来执行静态构造函数,它承诺只调用它们一次,并且在类中的任何方法运行之前进行调用。当有多个线程使用该类时,此保证相当棘手。
查看SSCLI20的CLR源代码,可以看到一个相当大的代码块专门用于提供此保证。它维护一个正在运行的静态构造函数列表,并受全局锁保护。一旦它在该列表中获得一个条目,它就切换到一个特定于类的锁,以确保没有两个线程可以运行构造函数。通过对一个指示构造函数已经运行的状态位进行双重检查锁定。有许多难以理解的代码提供异常保证。
这段代码不是免费的。将其添加到cctor本身的执行时间中,您将面临一些开销。与往常一样,请勿让这妨碍您的风格,此保证也是一个非常好的保证,您不希望自己提供。在修复之前,请先进行测量。

-1

我刚刚进行了一个小测试,以检查向我的某个类添加静态构造函数的影响。

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

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

问题在于,在其中一种实现中,该方法什么也不做,因此我可以设置一个预先完成的任务并每次返回它。
public sealed class Test1 : Base
{
    readonly Task _emptyTask;

    public Test1()
    {
        TaskCompletionSource<Object> source = new TaskCompletionSource<object>();
        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<Object> source = new TaskCompletionSource<object>();
        source.SetResult(null);
        _emptyTask = source.Task;
    }

    public override Task DoStuffAsync()
    {
        return _emptyTask;
    }
}

然后我想起了静态构造函数和性能方面的“问题”,经过一番研究(这就是我来到这里的原因),我决定进行一个小型基准测试:

Stopwatch sw = new Stopwatch();
List<Int64> test1list = new List<Int64>(), test2list = new List<Int64>();

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

因此,尽管存在静态构造函数,但好处大于问题。因此,请始终进行测量。


1
抱歉,以当前的形式来看,这个测试没有意义。在Test1中,每次创建Test1实例时都会创建一个新的TaskCompletionSource。而在Test2中,只有在静态构造函数中创建了一个。因此,Test1需要做更多的工作并创建额外的对象,这些对象需要被GC回收,这很可能是你时间差异的原因。 - Bunny83

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接