Вопрос по performance, .net, c# – Сравнительный анализ небольших примеров кода в C #, можно ли улучшить эту реализацию?
Довольно часто в 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 Совершенно очевидно, что предпочтителен подход, основанный на времени (в отличие от итераций), есть ли у кого-нибудь реализации, где проверки времени не влияют на производительность?
в соответствии с рекомендациями сообщества, не стесняйтесь вносить изменения в вики сообщества.
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.
GC.Collect
возвращается. Финализация ставится в очередь и затем запускается в отдельном потоке. Этот поток все еще может быть активным во время ваших тестов, влияя на результаты.
Если вы хотите убедиться, что завершение завершено до начала ваших тестов, вы можете позвонитьGC.WaitForPendingFinalizers
, который будет блокироваться до тех пор, пока не будет очищена очередь завершения:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.Collect()
еще раз
Collect
должен убедиться, что "завершенные" объекты также собраны.
вы можете выполнить «разогрев»посл GC.Collect call, не раньше. Таким образом, вы знаете, что .NET уже будет иметь достаточно памяти, выделенной из ОС для рабочего набора вашей функции.
Имейте в виду, что для каждой итерации вы вызываете метод без вложений, поэтому не забудьте сравнить тестируемые объекты с пустым телом. Вам также придется признать, что вы можете надежно рассчитывать время только в несколько раз дольше, чем вызов метода.
Кроме того, в зависимости от того, какие данные вы профилируете, вы можете выполнять синхронизацию в течение определенного времени, а не определенного количества итераций - это может привести к более легко сопоставимым числам. без необходимости иметь очень короткий пробег для лучшей реализации и / или очень длинный для худшего.
ызов @Delegate - это вызов виртуального метода. Недорого: ~ 25% минимального выделения памяти в .NET. Если вы заинтересованы в деталях, см.например. эта ссылк. Анонимные делегаты могут привести к использованию замыканий, которые вы даже не заметите. Опять же, доступ к полям закрытия заметнее, чем, например, доступ к переменной в стеке.
Пример кода, приводящий к использованию замыкания:
public void Test()
{
int someNumber = 1;
Profiler.Profile("Closure access", 1000000,
() => someNumber + someNumber);
}
Если вы не знаете о замыканиях, взгляните на этот метод в .NET Reflector.
IDisposable
.
что наиболее сложная проблема, которую необходимо решить с помощью таких методов, как учет крайних случаев и непредвиденных ситуаций. Например - «Как работают два фрагмента кода при высокой загрузке процессора / использовании сети / перегрузке диска / и т. Д.» Они отлично подходят для базовых логических проверок, чтобы увидеть, работает ли определенный алгоритм Значительно быстрее другого. Но для правильной проверки большей части производительности кода вам нужно создать тест, который измеряет конкретные узкие места этого конкретного кода.
Я бы все же сказал, что тестирование небольших блоков кода часто дает небольшую отдачу от инвестиций и может стимулировать использование слишком сложного кода вместо простого обслуживаемого кода. Написание понятного кода, который другие разработчики, или я сам через 6 месяцев, можем понять, будет иметь больше преимуществ в производительности, чем высоко оптимизированный код.
func()
Несколько раз для разминки, а не один.
подходит ли среда выполнения для тестирования производительности (например, определить, подключен ли отладчик или отключена оптимизация 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. Также естьСообщение блог.
Если вы спешите, я предлагаю вам взять образец пакета и просто изменить делегатов образца по мере необходимости. Если вы не спешите, возможно, стоит почитать сообщение в блоге, чтобы понять детали.
который вы тестируете, и платформы, на которой он работает, вам может потребоваться учесть Как выравнивание кода влияет на производительность. Для этого, вероятно, потребуется внешняя оболочка, которая запускала тест несколько раз (в отдельных доменах приложений или процессах?), В некоторых случаях сначала вызывая «код дополнения», чтобы заставить его быть скомпилированным JIT, чтобы код был сравнительный тест для выравнивания по-другому. Полный результат теста даст наилучшие и наихудшие моменты времени для различных выравниваний кода.
что одно измерение может ответить на все ваши вопросы. Вы должны измерить несколько раз, чтобы получить эффективную картину ситуации, особенно в языке мусора, подобного 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);
}
Но более важным, чем рекомендация каких-либо конкретных возможных дополнительных измерений для профилирования, является идея, что нужно измерять несколько различных статистических данных, а не только один вид статистики.