CLR, IL, JIT, AOT, tiered compilation
↑ 2.2 .NET runtime: CLR, память, GC · → Следующая
Зачем это на собесе
«Что происходит между
dotnet runи выполнением вашего метода?» — вопрос на понимание платформы. Ждут цепочку: компилятор → IL и метаданные → загрузка сборок CLR → JIT → машинный код, а также осознанный выбор между JIT и Native AOT.
Объяснение
Путь от C# до машинного кода
- Компилятор Roslyn превращает C# в IL (Intermediate Language, CIL) и метаданные. Результат — сборка (
.dll/.exe), не зависящая от процессора и ОС. - CLR (Common Language Runtime) при запуске загружает сборки, проверяет метаданные, разрешает типы.
- JIT-компилятор (Just-In-Time) при первом вызове метода переводит его IL в машинный код текущего процессора и кэширует результат в памяти процесса.
- Среда выполнения предоставляет GC, потоки, исключения, безопасность типов, взаимодействие с нативным кодом.
C# → (Roslyn) → IL + метаданные → (CLR загружает) → (JIT) → машинный код → выполнениеОдин и тот же IL работает на Windows, Linux, macOS, x64 и ARM: платформенно-зависима только среда выполнения.
IL и метаданные
- IL — стековая байткод-подобная инструкция (
ldarg,add,call,box,newobj). - Метаданные описывают типы, члены, ссылки на другие сборки; рефлексия читает именно их.
- Посмотреть IL:
ildasm, ILSpy, dotPeek, sharplab.io. Полезно, чтобы увидетьbox, замыкания, state machineasync.
JIT и tiered compilation
JIT должен быть быстрым при старте, но давать быстрый код в долгой работе. Решение — многоуровневая компиляция (tiered compilation, включена по умолчанию):
- Tier 0: быстрая компиляция без тяжёлых оптимизаций (быстрый старт).
- Tier 1: горячие методы (вызвано ~30 раз) перекомпилируются с полными оптимизациями в фоновом потоке.
- Dynamic PGO (.NET 8 по умолчанию): рантайм собирает профиль (какие ветки, какие типы в virtual-вызовах) в tier 0 и использует его при оптимизации tier 1: девиртуализация, inlining, выбор горячих веток.
- OSR (on-stack replacement): метод с долгим циклом переключается на оптимизированную версию «на лету».
- ReadyToRun (R2R): часть кода предкомпилируется в сборку заранее (быстрее старт, чуть менее оптимально; затем tier 1 перекомпилирует горячее).
<PropertyGroup>
<TieredPGO>true</TieredPGO>
<TieredCompilation>true</TieredCompilation>
<PublishReadyToRun>true</PublishReadyToRun> <!-- ускорить холодный старт -->
</PropertyGroup>Native AOT
Компиляция заранее в один нативный исполняемый файл без JIT и без установленного runtime.
<PublishAot>true</PublishAot>| JIT (по умолчанию) | Native AOT | |
|---|---|---|
| Время старта | сотни мс | десятки мс |
| Память | больше | меньше |
| Пиковая производительность | выше (PGO, динамическая оптимизация) | ниже или равна |
| Размер | нужен runtime, файлы больше | один файл, trimming |
| Ограничения | нет | нет динамической загрузки, ограниченная рефлексия, нужны source generators |
| Подходит для CLI-утилит, serverless, сервисов с частым холодным стартом; для обычных долгоживущих API чаще достаточно JIT + ReadyToRun. |
Полезные факты
- Первый вызов метода дороже: JIT-компиляция (эффект «холодного старта»). Прогрев (warm-up) запросов перед приёмом трафика уменьшает первые долгие ответы.
- Inlining: JIT встраивает маленькие методы;
[MethodImpl(MethodImplOptions.AggressiveInlining)]подсказка, но не гарантия. - Bounds check elimination, девиртуализация
sealed-классов — причины, по которымsealedбывает быстрее. System.Runtime.InteropServices/ P/Invoke: вызовы нативного кода; маршалинг стоит времени.
Нюансы и подводные камни
- Медленные первые запросы после деплоя — JIT + холодные кэши. Лечится ReadyToRun, прогревом, tiered PGO.
- Бенчмарки на холодном процессе врут: используйте BenchmarkDotNet, который прогревает и делает статистику.
- Native AOT ломает код с рефлексией, динамической загрузкой сборок и
Expression.Compile; библиотеки должны быть AOT-совместимы. TieredCompilation=falseускоряет пиковую производительность в некоторых случаях, но замедляет старт.- Отладочная сборка (Debug) отключает многие оптимизации JIT: измеряйте только Release.
Практика
- Сравнить время старта простого API на JIT, ReadyToRun и Native AOT.
- Посмотреть IL метода с
asyncи с замыканием на sharplab.io и найти state machine и display class. - Измерить первый и сотый вызов метода через
Stopwatch.
Вопросы с ответами
Что такое IL и зачем он нужен?
Промежуточный язык, в который компилируется C#. Он не зависит от процессора и ОС: JIT переводит его в машинный код конкретной платформы, поэтому одна сборка работает везде.
Что делает JIT и что такое tiered compilation?
JIT переводит IL в машинный код при первом вызове метода. Tiered compilation сначала быстро компилирует все методы без оптимизаций (tier 0), а горячие перекомпилирует с полными оптимизациями (tier 1), сочетая быстрый старт и высокую пиковую скорость.
Что такое Native AOT и когда его использовать?
Компиляция в нативный бинарник заранее, без JIT и runtime. Быстрый старт и малая память, но ограничения на рефлексию и динамическую загрузку. Подходит для CLI, serverless, контейнеров с частыми запусками.
Чем ReadyToRun отличается от Native AOT?
ReadyToRun добавляет предкомпилированный код в обычную сборку, но JIT и runtime остаются (быстрее старт, затем tier 1 оптимизирует). Native AOT полностью убирает JIT и runtime.
Почему первые запросы после старта медленные?
Методы ещё не скомпилированы JIT, кэши пусты, пулы соединений не созданы. Помогают ReadyToRun, прогрев и dynamic PGO.
Связанные темы
- Следующая: Стек и куча, boxing и unboxing
- Платформа .NET: Платформа .NET: SDK, runtime, версии, сборки, NuGet, solution и csproj
- Source generators и AOT: Source generators и Roslyn (как в analytics_generator)
- Производительность .NET: 7.1 Производительность .NET и кэширование