У меня с коллегами нередко возникают разговоры о быстродействии того или иного участка кода. Часто они перерастают в спор о том, какое же решение работает более быстро. И в этой ситуации чаще лучше набросать простенький бенчмарк, чем вести долгие философские разговоры о производительности. К сожалению, очень многие не умеют бенчмаркать правильно, в результате чего их тесты могут показать в корне неверные результаты. Поэтому мне хотелось бы обсудить наиболее важные вещи, которые нужно учитывать при составлении грамотного бенчмарка на C#.
- Release mode without debugging
Одной из самых распространённых ошибок при тестах производительности является запуск программы в Debug mode. Результаты, полученные таким образом могут в принципе не соответствовать действительности. Дело в том, что в Debug build компилятор C# добавляет множество IL-команд, которые могут значительно сказаться на производительности. Кроме того, запускать следует обязательно без отладки (Ctrl+F5 из студии, а ещё лучше — из консоли). Если запустить приложение с отладкой (F5 из студии), то отладочный код также сильно попортит результаты вашего бенчмарка. Более подробное описание этой проблемы можно найти у Эрика Липперта. - Прогрев кеша процессора
Запуск вашего теста должен осуществляться обязательно на прогретом кеше. Для этого необходимо перед замером времени запустить тест вхолостую несколько раз. Некоторые в порядке прогрева делают только один-два холостых запуска, но в реальности порой требуется 10-15 запусков, чтобы кеш действительно прогрелся. Лучше всего греть кеш до тех пор, пока колебания замеров времени от запуска к запуску не станут меньше некоторого небольшого процента. Результаты бенчмарка на непрогретом кеше могут быть дольше реальных в несколько десятков раз. - Запуск на одном процессоре
Также следует помнить, что мы живём в многопроцессорном мире, а у каждого процессора есть свой кеш. Для того, чтобы заставить бенчмарк выполняться только на одном процессоре нужно выставить соответствующую ProcessorAffinity-маску:Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
- Stopwatch, а не DateTime
Хорошей практикой является мерить время работы программы через Stopwatch. Подробнее можете почитать у того же Эрика Липперта или на dotnetperls. - Высокий приоритет
Не забывайте, что помимо вашей программы на компьютере выполняются также десятки других приложений. Лучше бы наиболее «тяжёлые» из них выключить на время теста, а ещё лучше задать в вашем приложений высокий приоритет для процесса и потока:Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; Thread.CurrentThread.Priority = ThreadPriority.Highest;
- Сборка мусора
Не забывайте про сборщик мусора, который может внезапно запуститься, когда его совсем не ждёшь. И если сборка мусора объектов из текущего теста должна учитываться при замерах времени, то сборка мусора объектов из предыдущего теста нам вовсе не нужна. Для этого хорошо бы перед тестом вызватьGC.Collect()
. А лучше бы его вызывать два раза, чтобы все корневые объекты перешли во второе поколение (подробнее можете почитать тут). Дождаться окончания выполнения всех финализаторов (если таковые имеются) тоже не помешает (в этом нам поможетGC.WaitForPendingFinalizers()
).
Я перечислил некоторые важные моменты, которые необходимо помнить при написании бенчмарка. Я люблю побенчмаркить, но каждый раз заново в каждом новом приложении обеспечивать чистоту эксперимента меня утомляет. Кроме того, хочется получить результаты теста в красивом виде, а возиться каждый раз с форматированием неохота. Поэтому я написал простенький проект BenchmarkDotNet (доступен на GitHub), который в удобном виде позволяет оценить производительность интересующих нас участков кода — достаточно лишь описать целевые методы. Надеюсь, этот проект поможет другим C#-разработчиком правильно оценить производительность их кода. Если же вы можете подсказать дополнительные вещи, которые следует учитывать для корректных замеров времени, то буду рад PullRequest-ам.
Автор: DreamWalker