Вопрос по performance, .net, c# – Сравнительный анализ небольших примеров кода в C #, можно ли улучшить эту реализацию?

102

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

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

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

<code>  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);
    }
</code>

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

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

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

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

Ваш Ответ

11   ответов
91

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

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;
}

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

Возможно, вы захотите развернуть цикл несколько раз, например, 10, чтобы минимизировать издержки цикла. Mike Dunlavey
Я только что обновил, чтобы использовать Stopwatch.StartNew. Не функциональное изменение, но сохраняет одну строку кода. LukeH
@ Люк, большие перемены (хотелось бы +1). @ Майк, я не уверен, я подозреваю, что издержки виртуального вызова будут намного выше, чем сравнение и назначение, поэтому разница в производительности будет незначительно Sam Saffron
Я бы предложил вам передать счетчик итераций в Action и создать там цикл (возможно, даже развернутый). Если вы измеряете относительно короткую операцию, это единственный вариант. И я бы предпочел видеть обратную метрику - например, количество проходов / сек. Alex Yakunin
Что ты думаешь о показе среднего времени? Примерно так: Console.WriteLine («Среднее время, прошедшее {0} мс», watch.ElapsedMilliseconds / iterations); rudimenter
22

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

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

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
ЗачемGC.Collect() еще раз colinfang
@ colinfang Потому что объекты, которые «завершаются», не обрабатываются GC финализатором. Итак, второйCollect должен убедиться, что "завершенные" объекты также собраны. MAV
15

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

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

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

Хорошие моменты, вы бы хотели иметь реализацию, основанную на времени? Sam Saffron
6

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

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

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

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

Интересные моменты, но как бы вы создали метод Profile () многократного использования, если вы не передаете делегата? Существуют ли другие способы передачи произвольного кода в метод? Ash
Мы используем «используя (новое измерение (...)) {... измеренный код ...}». Таким образом, мы получаем объект Measurement, реализующий IDisposable вместо передачи делегата. Видеть Code.google.com / р / dataobjectsdotnet / источник / просмотр / Xtensive.Core / ... Alex Yakunin
Это не приведет к проблемам с замыканиями. Alex Yakunin
@ AlexYakunin: ваша ссылка не работает. Не могли бы вы включить код для класса измерений в свой ответ? Я подозреваю, что независимо от того, как вы это реализуете, вы не сможете запускать код для многократного профилирования с этим подходом IDisposable. Однако это действительно очень полезно в ситуациях, когда вы хотите измерить, как работают различные части сложного (переплетенного) приложения, при условии, что вы помните, что измерения могут быть неточными и непоследовательными при запуске в разное время. Я использую тот же подход в большинстве своих проектов. ShdNx
Требование выполнить тест производительности несколько раз очень важно (разминка + многократные измерения), поэтому я также перешел на подход с делегатом. Более того, если вы не используете замыкания, вызов делегата происходит быстрее, чем вызов метода интерфейса в случае сIDisposable. Alex Yakunin
6

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

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

significant - один из тех терминов, который действительно загружен. иногда имеет значение реализация, которая на 20% быстрее, иногда она должна быть в 100 раз быстрее, чтобы быть значимой. Согласна с вами, для ясности смотрите: / Stackoverflow.com вопросы / 1018407 / ... Sam Saffron
В данном случае значимо не все, что загружено. Вы сравниваете одну или несколько одновременных реализаций, и если разница в производительности этих двух реализаций не является статистически значимой, не стоит переходить на более сложный метод. Paul Alexander
5

func() Несколько раз для разминки, а не один.

Намерение состояло в том, чтобы обеспечить выполнение jit-компиляции. Какое преимущество вы получаете от многократного вызова func до измерения? Sam Saffron
Чтобы дать JIT шанс улучшить свои первые результаты. Alexey Romanov
.NET JIT не улучшает свои результаты с течением времени (как это делает Java). Он преобразует метод из IL в Assembly только один раз при первом вызове. Matt Warren
1

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

выполняется до измерения Sam Saffron
4
Предложения по улучшению

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

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

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

Считая №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

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

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

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

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

1

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

1

а, стоит ли его устанавливатьGCSettings.LatencyMode?

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

0

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

Другой ответ дает хороший способ измерения базовой производительности.

static void Profile(string description, int iterations, Action func) {
    // 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);
}

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

static void ProfileGarbageMany(string description, int iterations, Action func) {
    // 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();
    }
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

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

static void ProfileGarbage(string description, int iterations, Action func) {
    // 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();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

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

Похожие вопросы