Прежде чем переписывать foreach, обновите бенчмарк

В PR предлагают заменить обычный foreach ручным циклом: «Так меньше аллокаций». Я бы сначала спросил, на какой версии .NET это измеряли.

15 сентября Stephen Toub опубликовал разбор производительности .NET 11. Меня зацепил пример с перебором IEnumerable, который хранится в поле экземпляра. В приведённом бенчмарке на .NET 10 создаётся объект перечислителя в куче, а на .NET 11 эта аллокация исчезает. Исходный цикл тот же.

За этим стоит расширение conditional escape analysis. JIT лучше распознаёт случаи, когда объект не покидает оптимизируемый участок, даже если рядом остаётся запасной путь с интерфейсными вызовами. Это не обещание убрать аллокации у любого foreach.

Для меня здесь повод пересмотреть сам подход к performance-PR. Усложнение кода остаётся в репозитории надолго, а причина, ради которой его внесли, иногда исчезает после обновления runtime.

Особенно легко пропустить это с AI-агентом: попросил «оптимизировать горячий путь», получил ручные циклы и более сложный код. Выглядит убедительно. Только сравнивать нужно на том runtime, который команда собирается эксплуатировать.

Я бы попросил к такому PR два замера: исходный и предложенный вариант на целевой версии .NET. Потом проверил бы нагрузочный сценарий сервиса: CPU, аллокации, паузы GC и p99. Ускорение маленького цикла может вообще не проявиться в endpoint, который ждёт PostgreSQL.

Есть и неприятная тонкость: прогретый микробенчмарк не показывает поведение сразу после рестарта. Для сервиса с частым масштабированием холодный старт тоже стоит измерять. И в статье используется .NET 11 RC1, так что это повод для стенда, а не для обновления прода вслепую.

Мне проще принять обычный foreach с измерениями, чем хитрый цикл с объяснением «так всегда быстрее».

У вас при обновлении .NET пересматривают старые ручные оптимизации или они остаются навсегда?

Разбор Stephen Toub

#dotnet #backend #AI