Как ошибки в бенчмарке могут привести к неправильным выводам
#csharp #performance #benchmark #bestpracticesВидел я однажды пост в LinkedIn, заголовок которого утверждал, что .NET 9 медленнее, чем .NET 8. Сильное заявление. Проверять я его конечно буду. Ведь я сам большой любитель замеров производительности. Перейдём сразу к тому, что не так с бенчмарком.
❌ Методы не возвращают результат
Современные компиляторы умные. Они могут понять, когда выполняемый код не влияет на результат программы, и просто удаляют его. Такая оптимизация называется dead-code elimination (DCE). В данном случае, метод DoSomeThing
фактически не делает ничего, поэтому компилятор удаляет его в методе For
. Но в методе ForEach_Linq
такое сделать нельзя, т.к. в нём создаётся делегат. В результате получается сравнение методов с разным поведением (рисунок 2).
✅ Всегда возвращайте результат из методов
Исправленный вариант может выглядеть так. Нам неважно, что будет в переменной sum. Главное, что её использование предотвращает DCE и не оказывает значительного влияния на результаты бенчмарка.
❌ Слепое сравнивание foreach
Как мы знаем, foreach
предназначен для итерации по коллекциям, которые возвращают Enumerator
. Но даже если коллекция возвращает Enumerator
, это не гарантирует, что он будет использоваться. Рассмотрим пример на рисунке 3:
foreach
с массивом будет преобразован вwhile
c индексаторов;foreach
со списком – вwhile
сList<int>.Enumerator
;foreach
сICollection<int>
– вwhile
сIEnumerator<int>
.
✅ Сравнивать производительность foreach нужно с разными типами
Поскольку foreach
компилируется в различный код в зависимости от коллекции, производительность может значительно отличаться. Поэтому важно сравнивать каждый тип отдельно. Не исключаю, что эта деталь реализации может измениться в будущем.
❌ Передача метода в виде параметра
При передаче метода в виде параметра создаётся делегат. В нашем примере – Action<int>
(рисунок 4). Делегат, как известно ссылочный тип, то есть его создание – аллокация памяти, которая влияет на результаты бечмарка.
✅ При передаче метода в виде параметра нужно заранее создать экземпляр делегата
Правильнее было бы проинициализировать делегат в методе GlobalSetup
, который выполняется перед бенчмарками.
Выводы
Теперь посмотрим на результаты исправленного бенчмарка. Bad – изначальный бенчмарк, Better – после исправления.
В бенчмарке Bad, который я запустил на своём ноутбуке, метод ForEach_LinqParallel
отработал на .NET 9 чуть быстрее, чем на .NET 8. То есть та разница в, о которой говорил автор, была в рамках погрешности.
В бенчмарке Better, метод ForEach_LinqParallel
отработал на .NET 9 медленнее чем на .NET 8 на 90 мкс. Такую разницу я бы тоже отнёс к погрешности и не стал акцентировать внимание.
В целом, результаты во всех трёх версиях .NET кажутся плюс-минус одинаковыми. Поэтому, я бы не стал заявлять о том, что .NET 9 медленнее .NET 8.