Статические конструкторы вызывают накладные расходы на производительность?

Недавно прочитано в статье о dotnetpearls.com здесь, в котором говорится, что статические ctors занимают значительное количество ударов по производительности.

Не может понять, почему?

Ответы

Ответ 1

Я думаю, что "существенный" является преувеличением в большинстве случаев использования.

Наличие статического конструктора (даже если оно ничего не делает) влияет на время инициализации типа из-за наличия/отсутствия флага beforefieldinit. Существуют более строгие гарантии времени, когда у вас есть статический конструктор.

Для большинства кодов я бы предложил, чтобы это не имело большого значения, но если вы сильно зацикливаетесь и получаете доступ к статическому члену класса, он может это сделать. Лично я бы не стал слишком беспокоиться об этом - если у вас есть подозрение, что это актуально в вашем реальном приложении, тогда проверьте его, а не гадать. Микробенчмарки, скорее всего, преувеличивают эффект здесь.

Стоит отметить, что .NET 4 ведет себя по-другому по отношению к предыдущим версиям, когда дело доходит до инициализации типа - поэтому любые тесты должны действительно показывать разные для того, чтобы быть релевантным.

Ответ 2

Хорошо, я только что повторил его тест.

Для 1000000000 итераций с конструкцией DEBUG я получаю:

  • 4s для его статического класса со статическим Конструктор
  • 3.6s тот же класс с прокомментированным статическим конструктором
  • 2.9s с классом нестатический (и создание экземпляра перед итерация) либо статическим конструктор или нет

То же самое с конструкцией RELEASE подчеркивает разницу:

  • Статический класс со статическим конструктором: 4046.875мс
  • Статический класс без статического конструктора: 484.375мс
  • Экземпляр со статическим конструктором: 484.375ms
  • Экземпляр без статического конструктора: 484.375мс

Ответ 3

CLR обеспечивает довольно сильную гарантию на выполнение статических конструкторов, а promises - только один раз и до запуска любого метода в классе. Эта гарантия довольно сложна для реализации, когда в классе есть несколько потоков.

Заглядывая в исходный код CLR для SSCLI20, я вижу довольно большой фрагмент кода, посвященный предоставлению этой гарантии. Он поддерживает список запущенных статических конструкторов, защищенных глобальной блокировкой. Когда он получает запись в этом списке, он переключается на блокировку класса, которая гарантирует, что ни один из двух потоков не может запускать конструктор. Двойная проверка блокировки на бит состояния, который указывает, что конструктор уже запущен. Множество неиспользуемых кодов, обеспечивающих гарантии исключений.

Ну, этот код не приходит бесплатно. Добавьте его к времени выполнения для самого cctor, и вы смотрите на некоторые издержки. Как всегда, не позволяйте этому судороги ваш стиль, эта гарантия также очень хорошая, которую вы не хотели бы предоставлять самим себе. И измерьте, прежде чем исправлять.

Ответ 4

Я просто сделал небольшой тест, чтобы проверить влияние добавления статического конструктора в один из моих классов.

У меня есть базовый класс, который выглядит так:

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

Таким образом, несмотря на наличие статического конструктора, преимущество перевешивает проблему. Так что всегда измеряйте.