Сравнительный анализ небольших примеров кода в C #, можно ли улучшить эту реализацию? - PullRequest
102 голосов
/ 26 июня 2009

Довольно часто на SO я обнаруживаю, что сравниваю небольшие куски кода, чтобы увидеть, какая реализация наиболее быстрая.

Довольно часто я вижу комментарии о том, что тестовый код не учитывает джиттинг или сборщик мусора.

У меня есть следующая простая функция бенчмаркинга, которую я медленно развивал:

  static void Profile(string description, int iterations, Action func) {
        // warm up 
        func();
        // clean up
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++) {
            func();
        }
        watch.Stop();
        Console.Write(description);
        Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
    }

Использование:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});

Есть ли в этой реализации недостатки? Достаточно ли хорошо, чтобы показать, что реализация X быстрее, чем реализация Y по Z итераций? Можете ли вы придумать, как бы вы могли это улучшить?

EDIT Совершенно очевидно, что предпочтителен подход, основанный на времени (в отличие от итераций), есть ли у кого-нибудь реализации, где проверки времени не влияют на производительность?

Ответы [ 11 ]

91 голосов
/ 26 июня 2009

Вот измененная функция: в соответствии с рекомендациями сообщества, не стесняйтесь вносить изменения в вики сообщества.

static double Profile(string description, int iterations, Action func) {
    //Run at highest priority to minimize fluctuations caused by other processes/threads
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
    Thread.CurrentThread.Priority = ThreadPriority.Highest;

    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
    return watch.Elapsed.TotalMilliseconds;
}

Убедитесь, что вы скомпилировали в Release с включенной оптимизацией и запустили тесты вне Visual Studio . Эта последняя часть важна, потому что JIT печатает свои оптимизации с подключенным отладчиком, даже в режиме Release.

22 голосов
/ 26 июня 2009

Финализация не обязательно будет завершена до того, как GC.Collect вернется. Финализация ставится в очередь и затем запускается в отдельном потоке. Этот поток все еще может быть активным во время ваших тестов, влияя на результаты.

Если вы хотите убедиться, что завершение завершено до начала ваших тестов, вы можете вызвать GC.WaitForPendingFinalizers, который будет блокироваться до тех пор, пока очередь очистки не будет очищена:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
15 голосов
/ 26 июня 2009

Если вы хотите исключить взаимодействия GC из уравнения, вы можете выполнить «прогрев» вызова после вызова GC.Collect, но не раньше. Таким образом, вы знаете, что .NET уже будет иметь достаточно памяти, выделенной из ОС для рабочего набора вашей функции.

Имейте в виду, что для каждой итерации вы вызываете не встроенный вызов метода, поэтому не забудьте сравнить тестируемые объекты с пустым телом. Вам также придется признать, что вы можете надежно рассчитывать время только в несколько раз дольше, чем вызов метода.

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

6 голосов
/ 26 июня 2009

Я думаю, что наиболее сложная проблема, которую необходимо решить с помощью таких методов, как учет крайних случаев и неожиданностей. Например - «Как работают два фрагмента кода при высокой загрузке процессора / использовании сети / перегрузке диска / и т. Д.» Они отлично подходят для базовой логической проверки, чтобы увидеть, работает ли конкретный алгоритм на значительно * на 1002 * быстрее, чем другой. Но для правильного тестирования большей части кода вам нужно создать тест, который измеряет конкретные узкие места этого конкретного кода.

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

6 голосов
/ 26 июня 2009

Я бы вообще не передавал делегата:

  1. Вызов делегата - это вызов виртуального метода. Недорого: ~ 25% минимального выделения памяти в .NET. Если вам интересны подробности, см. например. эта ссылка .
  2. Анонимные делегаты могут привести к использованию замыканий, которые вы даже не заметите. Опять же, доступ к полям закрытия заметнее, чем, например, доступ к переменной в стеке.

Пример кода, приводящего к использованию замыкания:

public void Test()
{
  int someNumber = 1;
  Profiler.Profile("Closure access", 1000000, 
    () => someNumber + someNumber);
}

Если вы не знаете о замыканиях, взгляните на этот метод в .NET Reflector.

5 голосов
/ 22 июля 2009

Я бы позвонил func() несколько раз на разминку, а не один.

4 голосов
/ 14 сентября 2014

Предложения по улучшению

  1. Определение того, подходит ли среда выполнения для тестирования производительности (например, определение, подключен ли отладчик или отключена оптимизация jit, что может привести к неправильным измерениям).

  2. Измерение отдельных частей кода независимо (чтобы точно определить узкое место).

  3. Сравнение различных версий / компонентов / фрагментов кода (в первом предложении вы говорите: «... тестирование небольших фрагментов кода для определения того, какая реализация является самой быстрой.»).

Относительно № 1:

  • Чтобы определить, подключен ли отладчик, прочитайте свойство System.Diagnostics.Debugger.IsAttached (Не забудьте также обработать случай, когда отладчик изначально не подключен, но через некоторое время подключен).

  • Чтобы определить, отключена ли оптимизация jit, прочитайте свойство DebuggableAttribute.IsJITOptimizerDisabled соответствующих сборок:

    private bool IsJitOptimizerDisabled(Assembly assembly)
    {
        return assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
            .Select(customAttribute => (DebuggableAttribute) customAttribute)
            .Any(attribute => attribute.IsJITOptimizerDisabled);
    }
    

Относительно № 2:

Это можно сделать разными способами. Одним из способов является предоставление нескольких делегатов, а затем их измерение по отдельности.

Относительно № 3:

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

Один из способов сделать это - вернуть результат теста в виде строго типизированного объекта, который можно легко использовать в разных контекстах.


Etimo.Benchmarks

Другой подход заключается в использовании существующего компонента для выполнения тестов. На самом деле, в моей компании мы решили выпустить наш инструмент для тестирования в открытом доступе. По своей сути он управляет сборщиком мусора, дрожанием, прогревом и т. Д., Как и некоторые другие ответы здесь. У этого также есть три особенности, которые я предложил выше. Он управляет несколькими вопросами, обсуждаемыми в блоге Эрика Липперта 1050 *.

Это пример вывода, где сравниваются два компонента и результаты записываются в консоль. В этом случае два сравниваемых компонента называются 'KeyedCollection' и 'MultiplyIndexedKeyedCollection':

Etimo.Benchmarks - Sample Console Output

Существует пакет NuGet , образец пакета NuGet , а исходный код доступен по адресу GitHub . Существует также сообщение в блоге .

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

1 голос
/ 15 сентября 2014

Если вы пытаетесь устранить влияние сбора мусора после завершения теста, стоит ли устанавливать GCSettings.LatencyMode?

Если нет, и вы хотите, чтобы влияние мусора, созданного в func, было частью эталонного теста, то не следует ли также принудительно собирать данные в конце теста (внутри таймера)?

1 голос
/ 27 июня 2012

В зависимости от кода, который вы тестируете, и платформы, на которой он работает, вам может потребоваться учесть , как выравнивание кода влияет на производительность . Для этого, вероятно, потребуется внешняя оболочка, которая запускала тест несколько раз (в отдельных доменах приложений или процессах?), В некоторых случаях сначала вызывая «код заполнения», чтобы заставить его быть скомпилированным JIT, чтобы код был сравнительный тест для выравнивания по-другому. Полный результат теста дал бы наилучшие и наихудшие моменты времени для различных выравниваний кода.

1 голос
/ 26 июня 2009

Вы также должны выполнить прогон прогрева перед фактическим измерением, чтобы исключить время, которое JIT-компилятор тратит на соединение вашего кода.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...