.NET Tutorial
Senior5 мин чтенияВопрос 20 из 25

CLR JIT vs AOT компиляция — отличия, NativeAOT?

Путь от C# до машинного кода

Исходный код C#

Roslyn

IL-сборка (.dll)

JIT в рантайме
при первом вызове метода

ReadyToRun
crossgen при публикации

NativeAOT
ILCompiler при публикации

Машинный код в памяти процесса

Готовый код в сборке + IL
(JIT может улучшить)

Нативный исполняемый файл
без JIT и IL

Исходный код C#

Roslyn

IL-сборка (.dll)

JIT в рантайме
при первом вызове метода

ReadyToRun
crossgen при публикации

NativeAOT
ILCompiler при публикации

Машинный код в памяти процесса

Готовый код в сборке + IL
(JIT может улучшить)

Нативный исполняемый файл
без JIT и IL

Три модели компиляции

Как работает JIT

При первом вызове метода вместо машинного кода вызывается заглушка (stub), которая запускает JIT-компилятор. Готовый код сохраняется, адрес в заглушке заменяется — последующие вызовы идут напрямую.

Tiered compilation

Компилировать всё с полными оптимизациями — долго, старт будет медленным. Поэтому (с .NET Core 3.0) используется многоуровневая компиляция:

первый вызов

метод горячий (30+ вызовов)

собран профиль

долгий цикл в методе

Tier 0 — быстро, без оптимизаций
(или готовый код ReadyToRun)

Tier 0 instrumented — сбор профиля
(типы, ветвления)

Tier 1 — полные оптимизации + PGO
инлайнинг, девиртуализация

On-Stack Replacement —
переход на оптимизированный код
прямо внутри цикла

первый вызов

метод горячий (30+ вызовов)

собран профиль

долгий цикл в методе

Tier 0 — быстро, без оптимизаций
(или готовый код ReadyToRun)

Tier 0 instrumented — сбор профиля
(типы, ветвления)

Tier 1 — полные оптимизации + PGO
инлайнинг, девиртуализация

On-Stack Replacement —
переход на оптимизированный код
прямо внутри цикла

Уровни компиляции метода

Dynamic PGO (Profile-Guided Optimization, включён по умолчанию с .NET 8): JIT видит, что в IShape.Area() в 95% случаев приходит Circle, и генерирует быструю ветку if (shape is Circle) { инлайн Circle.Area } else { виртуальный вызов }. AOT-компилятор так не умеет — у него нет данных о реальной нагрузке.

ReadyToRun: компромисс

XML
<PropertyGroup>
  <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
  • Сборки содержат предкомпилированный код → меньше работы JIT при старте (часто -30–50% времени старта).
  • IL остаётся → полная совместимость: рефлексия, Emit, всё работает.
  • Горячие методы всё равно перекомпилируются JIT-ом в Tier 1 с PGO.
  • Минус — сборки больше по размеру.

NativeAOT

XML
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>   <!-- меньше размер, если не нужна ICU -->
</PropertyGroup>
Shell
dotnet publish -c Release -r linux-x64
# результат: один исполняемый файл ~10–20 МБ для Minimal API

Компилятор анализирует всю программу целиком (whole program analysis), выбрасывает неиспользуемый код (trimming) и компилирует остальное в нативный код.

JIT ReadyToRun NativeAOT
Время старта Медленное (сотни мс) Быстрее Мгновенное (десятки мс)
Пиковая производительность Максимальная (PGO) Максимальная (после JIT) Хорошая, но без Dynamic PGO
Память Больше (JIT, метаданные) Больше Меньше
Размер Нужен рантайм Нужен рантайм, сборки больше Один файл, без рантайма
Рефлексия Полная Полная Ограниченная (только сохранённое trimmer-ом)
Reflection.Emit, Expression.Compile Да Да Нет / интерпретация
Динамическая загрузка сборок Да Да Нет
Кроссплатформенность бинарника Да (IL) Нет (под RID) Нет (под RID)

Ограничения NativeAOT на практике

C#
// Не работает без доработки: сериализация через рефлексию
JsonSerializer.Serialize(order);                          // warning IL2026/IL3050

// Работает: source generator
[JsonSerializable(typeof(Order))]
partial class AppJson : JsonSerializerContext { }
JsonSerializer.Serialize(order, AppJson.Default.Order);

// Minimal API: CreateSlimBuilder + генератор делегатов запросов
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJson.Default));

Что пока плохо поддерживается или требует обходных путей:

  • MVC-контроллеры (используют рефлексию) — только Minimal API.
  • EF Core — поддержка экспериментальная (precompiled queries, .NET 9+).
  • Библиотеки, использующие Emit: классический AutoMapper, Castle DynamicProxy (Moq), часть DI-контейнеров.
  • Встроенный DI поддерживается, System.Text.Json и ILogger — с генераторами.

Как выбрать

нет

да

да

нет

да

нет

Критичны время старта
и память? (serverless, CLI,
scale-to-zero)

Хочется ускорить старт
без ограничений?

Код и зависимости
AOT-совместимы?

NativeAOT

ReadyToRun

JIT по умолчанию
(максимум пиковой скорости)

нет

да

да

нет

да

нет

Критичны время старта
и память? (serverless, CLI,
scale-to-zero)

Хочется ускорить старт
без ограничений?

Код и зависимости
AOT-совместимы?

NativeAOT

ReadyToRun

JIT по умолчанию
(максимум пиковой скорости)

Выбор модели компиляции

Вопросы на засыпку

Почему JIT может генерировать более быстрый код, чем AOT?

JIT знает конкретный процессор (может использовать AVX-512, если он есть) и реальный профиль выполнения: какие типы приходят в виртуальные вызовы, какие ветки горячие. AOT должен компилировать под «средний» процессор и без знания нагрузки.

Что такое trimming и чем он опасен?

Удаление неиспользуемого кода при публикации (PublishTrimmed). Опасен для кода с рефлексией: тип или метод, используемый только через строку, будет вырезан, и в рантайме случится MissingMethodException. Анализатор выдаёт предупреждения, их нельзя игнорировать.

Что такое On-Stack Replacement?

Если метод с долгим циклом вызван один раз (например, Main с обработкой миллиона элементов), он так и остался бы в неоптимизированном Tier 0. OSR позволяет переключиться на оптимизированную версию прямо посреди выполнения цикла.

Можно ли отключить Tiered compilation?

Да: <TieredCompilation>false</TieredCompilation> — всё сразу компилируется с полными оптимизациями (медленнее старт, без PGO). Или <TieredPGO>false</TieredPGO>. Обычно настройки по умолчанию оптимальны.

Итог

JIT даёт максимальную пиковую скорость благодаря знанию реальной нагрузки, ReadyToRun ускоряет старт без ограничений, NativeAOT — мгновенный старт и минимальный размер ценой отказа от динамических возможностей. Для обычного веб-сервиса хватает JIT, для serverless и CLI — стоит рассмотреть NativeAOT.