Microsoft описала сотни улучшений производительности в .NET 11

Microsoft описала сотни улучшений производительности в .NET 11

Команда .NET в блоге devblogs.microsoft.com опубликовала очередной ежегодный пост о производительности, «Performance Improvements in .NET 11». Это продолжение серии, которую автор ведёт с версий .NET Core 2.0 и 2.1 и далее через .NET 5, 10: каждый год пост подробно разбирает, что именно ускорилось в рантайме и стандартных библиотеках. Материал обрамлён шуточной отсылкой к фильму «Это Spinal Tap» и сцене про усилитель, у которого «одиннадцать» громче любой «десятки», автор проводит параллель: .NET 11 не громче на пустом месте, как шутка про усилитель, а буквально на одну ступень производительнее благодаря реальной работе. Пост описывает «сотни» отдельных улучшений: убранная проверка границ массива, аллокация, которой больше не происходит, снятая блокировка, цикл, выполняющийся за меньшее число итераций, чем год назад, свёрнутое в константу сравнение, вынесенная из цикла избыточная проверка, слитые в одну несколько инструкций, обойдённый системный вызов, копирование массива, переданное на SIMD-инструкции.

Все примеры сопровождаются микробенчмарками на BenchmarkDotNet: автор объясняет, как их воспроизвести, установить .NET 10 и .NET 11, создать консольный проект, нацелить его одновременно на net10.0 и net11.0 (в проекте указаны конкретные версии пакетов: .NET 11, релиз-кандидат 11.0.0-rc.1.26425.128.NET 10, 10.0.12, BenchmarkDotNet, 0.16.0-preview.1) и запускать бенчмарки командой dotnet run с флагом --runtimes, сравнивающей обе версии рантайма бок о бок.

Подробно в доступном фрагменте поста раскрыт только первый раздел, про JIT-компилятор и так называемую «деабстракцию»: способность рантайма убирать накладные расходы абстракций, когда доказано, что эффект абстракции никак не проявляется. Разобран пример с виртуальным вызовом метода: когда переменная объявлена как абстрактный тип (в примере, Animal), а фактически в неё кладут Dog или Cat, компилятору при компиляции неизвестно, какой именно метод Speak() будет вызван, и он генерирует код с тремя зависимыми обращениями в память и непрямым вызовом через таблицу виртуальных методов (vtable), это медленнее прямого вызова и, что важнее, не даёт JIT встроить (заинлайнить) тело метода в место вызова, а значит закрывает путь к дальнейшим оптимизациям вроде свёртки констант и удаления мёртвого кода. JIT умеет доказывать конкретный тип статически, например, если объект только что создан через new Dog(), если класс запечатан (sealed) и не может иметь других потомков, либо (при использовании NativeAOT со сборкой всей программы целиком) если во всём приложении есть только один класс-наследник абстрактного типа. Когда статически доказать это нельзя, в дело вступает профилированная оптимизация (PGO): при первом вызове метод компилируется по «уровню 0» почти без оптимизаций, но со встроенными счётчиками, которые фиксируют, какие ветви кода реально выполняются и какие конкретные типы встречаются в месте виртуального вызова; если метод вызывается достаточно часто, рантайм запрашивает у JIT переоптимизированную версию, «уровень 1», которая уже учитывает накопленную статистику.

В доступном тексте поста нет конкретных цифр, процентов ускорения, миллисекунд или объёма сэкономленной памяти, по отдельным улучшениям: он обрывается на середине раздела про JIT, до перечисления результатов этих сотен оптимизаций.

Ключевые факты

  • Microsoft выпустила ежегодный обзор производительности .NET, за 2026 год это .NET 11, продолжение серии постов, которая ведётся с .NET Core 2.0
  • Пост описывает «сотни» отдельных точечных улучшений рантайма и библиотек: убранные проверки границ массива, лишние аллокации, блокировки, системные вызовы, инструкции, перенесённые на SIMD
  • Каждое улучшение сопровождается воспроизводимым микробенчмарком на BenchmarkDotNet; .NET 11 на момент публикации был релиз-кандидатом версии 11.0.0-rc.1.26425.128, сравнение идёт с .NET 10 версии 10.0.12
  • Подробно разобран раздел про JIT-компилятор: девиртуализация и инлайнинг виртуальных вызовов на примере Dog/Cat, а также профилированная оптимизация (PGO) с многоуровневой компиляцией (Tier 0 → Tier 1)
  • Доступный фрагмент текста обрывается до перечисления конкретных числовых результатов улучшений, только описание методики и один развёрнутый пример

Почему это важно

Пост продолжает ежегодную традицию команды .NET, подробно документировать работу над производительностью, которая иначе остаётся незаметной внутри релиза. JIT-компилятор компилирует IL-код в машинные инструкции для любого приложения на C#, F# или Visual Basic, поэтому даже одна убранная проверка или один снятый барьер памяти способны сказаться на огромном числе программ сразу, без изменений в их исходном коде.

Кому это важно

В первую очередь, разработчикам на .NET (C#, F#, Visual Basic), которым не нужно ничего менять в коде, чтобы получить часть ускорений «бесплатно» при переходе на новую версию рантайма. Также материал полезен инженерам, которые сами занимаются производительностью и хотят понять методику измерения на конкретных примерах JIT-оптимизаций.

Как это применить

Автор приводит пошаговую инструкцию: установить .NET 10 и .NET 11, создать консольный проект с BenchmarkDotNet, указать в .csproj двойной таргетинг net11.0 и net10.0 с версиями пакетов 11.0.0-rc.1.26425.128 и 10.0.12 соответственно, вставить код конкретного бенчмарка в Program.cs и запустить через dotnet run с флагом --runtimes net10.0 net11.0, это даёт бенчмарк side-by-side на обеих версиях рантайма без переписывания кода вручную под каждую.

Можно ли доверять

Источник, официальный блог .NET на devblogs.microsoft.com, то есть первичное сообщение от самой команды разработки, а не пересказ третьих лиц. Автор поста в доступном тексте не назван. Каждое улучшение в разделе про JIT подкреплено конкретным разбираемым примером кода и объяснением механизма (девиртуализация, инлайнинг, PGO), а не голым заявлением; сам автор в тексте прямо предупреждает, что это микробенчмарки и результаты будут зависеть от железа, ОС и настроек рантайма. При этом сами числовые результаты «сотен» улучшений в полученном фрагменте текста отсутствуют, материал обрывается раньше, чем до них доходит.

Риски и подводные камни

На момент публикации .NET 11 существовал в виде релиз-кандидата (версия 11.0.0-rc.1.26425.128), а не финального релиза, так что часть цифр и деталей может измениться до выхода стабильной версии. Сам автор оговаривает: приведённые числа, результаты микробенчмарков, измеряющих операции длительностью в доли секунды, и они чувствительны к конкретному железу, конфигурации ОС и рантайма, фоновой нагрузке на машину, то есть напрямую переносить их на реальное приложение нельзя без собственных замеров.

«Из всех мест, где можно улучшать производительность .NET, немногие дают такой же широкий эффект, как JIT-компилятор (компилятор «на лету»).»

— автор поста в блоге .NET