content

Видел я однажды пост в LinkedIn, заголовок которого утверждал, что .NET 9 медленнее, чем .NET 8. Сильное заявление. Проверять я его конечно буду. Ведь я сам большой любитель замеров производительности. Перейдём сразу к тому, что не так с бенчмарком.

❌ Методы не возвращают результат

Современные компиляторы умные. Они могут понять, когда выполняемый код не влияет на результат программы, и просто удаляют его. Такая оптимизация называется dead-code elimination (DCE). В данном случае, метод DoSomeThing фактически не делает ничего, поэтому компилятор удаляет его в методе For. Но в методе ForEach_Linq такое сделать нельзя, т.к. в нём создаётся делегат. В результате получается сравнение методов с разным поведением (рисунок 2).

✅ Всегда возвращайте результат из методов

Исправленный вариант может выглядеть так. Нам неважно, что будет в переменной sum. Главное, что её использование предотвращает DCE и не оказывает значительного влияния на результаты бенчмарка.

content

❌ Слепое сравнивание foreach

Как мы знаем, foreach предназначен для итерации по коллекциям, которые возвращают Enumerator. Но даже если коллекция возвращает Enumerator, это не гарантирует, что он будет использоваться. Рассмотрим пример на рисунке 3:

  • foreach с массивом будет преобразован в while c индексаторов;
  • foreach со списком – в while с List<int>.Enumerator;
  • foreach с ICollection<int> – в while с IEnumerator<int>.

✅ Сравнивать производительность foreach нужно с разными типами

Поскольку foreach компилируется в различный код в зависимости от коллекции, производительность может значительно отличаться. Поэтому важно сравнивать каждый тип отдельно. Не исключаю, что эта деталь реализации может измениться в будущем.

content

❌ Передача метода в виде параметра

При передаче метода в виде параметра создаётся делегат. В нашем примере – Action<int> (рисунок 4). Делегат, как известно ссылочный тип, то есть его создание – аллокация памяти, которая влияет на результаты бечмарка.

✅ При передаче метода в виде параметра нужно заранее создать экземпляр делегата

Правильнее было бы проинициализировать делегат в методе GlobalSetup, который выполняется перед бенчмарками.

content

Выводы

Теперь посмотрим на результаты исправленного бенчмарка. Bad – изначальный бенчмарк, Better – после исправления.

content

В бенчмарке Bad, который я запустил на своём ноутбуке, метод ForEach_LinqParallel отработал на .NET 9 чуть быстрее, чем на .NET 8. То есть та разница в, о которой говорил автор, была в рамках погрешности.

В бенчмарке Better, метод ForEach_LinqParallel отработал на .NET 9 медленнее чем на .NET 8 на 90 мкс. Такую разницу я бы тоже отнёс к погрешности и не стал акцентировать внимание.

В целом, результаты во всех трёх версиях .NET кажутся плюс-минус одинаковыми. Поэтому, я бы не стал заявлять о том, что .NET 9 медленнее .NET 8.