С#: Является ли этот класс бенчмаркинга точным?

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

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();

Любая обратная связь? Означает ли он, что он довольно стабильный и точный, или я что-то пропустил?

Ответ 1

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

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

Вы могли бы что-то сделать в этой последней точке, эталонный тест является одной из редких ситуаций, когда можно защитить вызов 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 мс).

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

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

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

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

Ответ 4

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

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

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