C#: Точен ли этот класс тестирования?

Я создал простой класс для сравнения некоторых моих методов. Но точно ли это? Я новичок в бенчмаркинге, сроках и так далее, поэтому подумал, что могу попросить кое-какие отзывы здесь Кроме того, если это хорошо, возможно, кто-то еще может использовать это также:)

public static class Benchmark
{
    public static IEnumerable<long> This(Action subject)
    {
        var watch = new Stopwatch();
        while (true)
        {
            watch.Reset();
            watch.Start();
            subject();
            watch.Stop();
            yield return watch.ElapsedTicks;
        }
    }
}

Вы можете использовать это так:

var avg = Benchmark.This(() => SomeMethod()).Take(500).Average();

Есть отзывы? Это выглядит довольно стабильно и точно, или я что-то пропустил?

4 ответа

Решение

Это примерно так же точно, как вы можете получить для простого теста. Но есть некоторые факторы, которые не находятся под вашим контролем:

  • нагрузка на систему от других процессов
  • состояние кучи до / во время теста

Вы могли бы что-то сделать с этим последним пунктом, эталонный тест является одной из редких ситуаций, когда вызов GC.Collect можно защитить. И вы могли бы позвонить subject один раз заранее, чтобы устранить любые проблемы JIT. Но для этого нужны звонки subject быть независимым.

public static IEnumerable<TimeSpan> This(Action subject)
{
    subject();     // warm up
    GC.Collect();  // compact Heap
    GC.WaitForPendingFinalizers(); // and wait for the finalizer queue to empty

    var watch = new Stopwatch();
    while (true)
    {
        watch.Reset();
        watch.Start();
        subject();
        watch.Stop();
        yield return watch.Elapsed;  // TimeSpan
    }
}

Для получения бонуса, ваш класс должен проверить System.Diagnostics.Stopwatch.IsHighResolution поле Если он выключен, у вас только очень грубое (20 мс) разрешение.

Но на обычном ПК с множеством сервисов, работающих в фоновом режиме, он никогда не будет очень точным.

Пара проблем здесь.

Во-первых, помните, что при первом запуске кода транзитивное замыкание его вызовов методов будет выполнено. Это означает, что первый прогон, вероятно, будет стоить дороже, чем каждый последующий прогон. В зависимости от того, тестируете ли вы "холодные" или "горячие" моменты, это может иметь значение. Я видел методы, в которых стоимость использования метода была выше, чем все остальные вызовы вместе взятые!

Во-вторых, помните, что сборщик мусора работает в другом потоке. Если вы производите мусор за один прогон, то затраты на его очистку могут быть не реализованы до последующих запусков. Поэтому вы не в состоянии учесть общую стоимость одного прогона, включив его в последующие прогоны.

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

Вы должны обязательно вернуть ElapsedMilliseconds вместо ElapsedTicks. Значение, возвращаемое ElapsedTicks, зависит от частоты секундомера, которая может быть разной в разных системах. Он не обязательно будет соответствовать свойству Ticks объекта Timespan или DateTime.

См. http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.elapsedticks.aspx.

Если вы хотите дополнительное разрешение тиков, вы должны вернуть watch.Elapsed.Ticks (т.е. Timestamp.Ticks) вместо watch.ElapsedTicks (это может быть одной из самых тонких потенциальных ошибок в.Net). Из MSDN:

Тики секундомера отличаются от DateTime.Ticks. Каждый тик в значении DateTime.Ticks представляет один интервал в 100 наносекунд. Каждый тик в значении ElapsedTicks представляет интервал времени, равный 1 секунде, деленной на частоту.

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

И последнее замечание, которое, вероятно, не относится к большинству применений этого класса: секундомер работает немного быстрее по сравнению с системным временем. На моем компьютере он опережает примерно на 5 секунд (то есть секунд, а не миллисекунд) после 24 часов, а на других машинах этот дрейф может быть еще больше. Так что немного вводить в заблуждение говорить, что это очень точно, когда на самом деле это просто очень детально. Для временных методов короткой продолжительности это, очевидно, не будет существенной проблемой.

И еще один последний момент, который, безусловно , актуален: я часто замечал, что во время бенчмаркинга я получал кучу времени выполнения, которые все кластеризованы в узком диапазоне значений (например, 80, 80, 79, 82 и т. Д.), но иногда что-то еще происходит в Windows (например, открытие другой программы или мой антивирус включается или что-то в этом роде), и я получаю дикое значение с другими (например, 80, 80, 79, 271, 80 и т. д.).). Я думаю, что простым решением этой проблемы является использование медианы ваших измерений вместо среднего. Я не знаю, поддерживает ли Linq это автоматически или нет.

Поскольку я не программист на C#, я не могу с достаточной точностью сказать, является ли этот класс подходящей реализацией для подсчета времени, которое занимает выполнение функции. Однако, есть вещи, которые нужно иметь в виду для повторяемости и точности.

Я не разбираюсь в различных тонкостях.NET Framework, но в зависимости от того, как он компилируется в нативный код, возможно, что любая компиляция повлияет на результаты тестов. Кроме того, наличие функции в кеше также может иметь значение. Таким образом, вы захотите зациклить свою функцию, чтобы убедиться, что нет компиляции и что все загружено и готово. Как только это будет сделано, вы сможете начать.

У других, вероятно, будет больше информации и знаний о.NET, чем у меня.

Другие вопросы по тегам