Підрозділ 1.4
JIT-компіляція
Пояснює JIT-компіляцію, проміжну мову CIL і те, як C#-код перетворюється на машинні інструкції під час виконання.
1.4. JIT-компіляція
Код C#, написаний програмістом, не виконується процесором безпосередньо. Він проходить два окремих етапи перетворення, перш ніж стати машинними інструкціями. Розуміння цих етапів допомагає пояснити, чому .NET поєднує кросплатформовість з високою продуктивністю.
Два кроки до виконання
Крок 1 — компілятор C# (під час розробки):
Файл .cs обробляє компілятор і перетворює його на CIL (Common Intermediate Language) — проміжну мову платформи. Результат зберігається у збірці: файл .dll або .exe. CIL — це не машинний код жодного конкретного процесора; це платформонезалежні інструкції, зрозумілі CLR.
Разом із CIL збірка містить метадані — повний опис типів, методів, полів, параметрів. Завдяки метаданим CLR розуміє структуру коду без потреби в заголовкових файлах (на відміну від C++).
Крок 2 — JIT-компілятор (під час виконання):
JIT (Just-In-Time) — «саме вчасно». Коли програма запущена і певний метод викликається вперше, JIT-компілятор перетворює його CIL на машинний код конкретного процесора. Скомпільований код кешується: при повторних викликах він виконується напряму, без повторної компіляції.
У сучасному .NET (з .NET Core 3.0) цей процес багаторівневий (tiered compilation). Перший раз JIT компілює метод швидко, майже без оптимізацій (рівень 0) — щоб програма стартувала якнайшвидше. Якщо метод виявляється «гарячим» (приблизно після 30 викликів), він перекомпілюється у фоні з повними оптимізаціями (рівень 1), а з .NET 8 — ще й з урахуванням зібраного під час роботи профілю (Dynamic PGO); наступні виклики йдуть уже в оптимізований код. Методи з довгими циклами, які викликаються рідко, оптимізуються «на льоту» через заміну коду посеред виконання (OSR). Крім того, бібліотеки самого .NET постачаються в форматі ReadyToRun — з уже готовим машинним кодом, тому значна частина коду при старті взагалі не проходить через JIT.
Покроковий процес JIT


Ключовий момент: JIT не компілює всю програму при запуску. Він компілює лише ті методи, які фактично викликаються під час конкретного сеансу роботи. Методи, до яких програма не звертається, залишаються у вигляді CIL і не витрачають час на компіляцію.
Демонстрація: що відбувається при запуску
Перший виклик SayHello() запускає JIT-компіляцію цього методу. Другий виклик — вже йде напряму у нативний код (рівня 0; до рівня 1 метод дійде, лише якщо його викликатимуть часто).
CIL і метадані — що всередині збірки
Збірка .dll — це не просто набір інструкцій. Вона містить дві принципово різні речі:
| Що | Для чого |
|---|---|
| CIL-інструкції | Платформонезалежний проміжний код, що JIT перетворює у машинний |
| Метадані | Опис типів, методів, полів, параметрів, атрибутів |
Метадані — це те, що дозволяє C# мати рефлексію: програма може запитати у CLR «які методи є у цьому класі?» прямо під час виконання:
Це можливо саме завдяки метаданим у збірці. У мовах без керованого середовища (C, C++) такої можливості за замовчуванням немає.
Переваги JIT-підходу
- Кросплатформовість: одна і та сама збірка запускається на будь-якій ОС, де є .NET runtime
- Адаптація до CPU: JIT знає, на якому процесорі запускається код, і може використовувати специфічні інструкції (AVX, SSE)
- Ощадливість: компілюється лише код, що реально виконується
- Оптимізації runtime: JIT бачить реальну поведінку програми — які типи насправді приходять у віртуальні виклики, які гілки виконуються частіше. Завдяки Dynamic PGO він може, наприклад, девіртуалізувати виклик для найчастішого типу й вбудувати (inline) його тіло. Сам inlining доступний і статичним компіляторам (C++, Native AOT); перевага JIT — у рішеннях на основі профілю виконання
JIT vs AOT


Окрім JIT, у сучасному .NET існує AOT (Ahead-Of-Time) — компіляція наперед, ще до запуску:
- JIT: збірка з CIL розповсюджується, машинний код генерується на машині користувача під час виконання
- AOT: нативний бінарний файл генерується заздалегідь; при запуску нічого компілювати не треба
AOT корисний для:
- AWS Lambda / хмарні функції — де важливий час холодного старту
- .NET MAUI і iOS — де JIT взагалі заборонений платформою
- Blazor WebAssembly — де код виконується у браузері (за замовчуванням там працює інтерпретатор IL; AOT-компіляція у WebAssembly вмикається окремо і пришвидшує обчислення ціною більшого розміру завантаження)
- CLI-утиліт — де потрібен один нативний бінарний файл без залежностей
Для більшості серверних і консольних застосунків JIT є стандартним і оптимальним вибором. AOT потрібен у специфічних сценаріях і вносить певні обмеження (наприклад, на рефлексію).
NativeAOT — нативний бінарний файл без JIT
NativeAOT (.NET 7+) — спеціальний режим публікації, що компілює весь застосунок у нативний машинний код заздалегідь, ще до розгортання. Результат — єдиний виконуваний файл без залежності від встановленого .NET runtime на машині користувача. Саме середовище виконання (GC, обробка винятків, типи) нікуди не зникає — воно вбудовується у файл; відсутній лише JIT. NativeAOT для консольних застосунків з'явився в .NET 7, для ASP.NET Core — у .NET 8.
Щоб опублікувати застосунок у режимі NativeAOT:
<!-- У файлі .csproj -->
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>dotnet publish -r win-x64 -c Release
# або
dotnet publish -r linux-x64 -c ReleaseРезультат — один нативний .exe (на Windows) або бінарний файл (на Linux), що запускається без встановленого .NET.
| Показник | JIT | NativeAOT |
|---|---|---|
| Час старту | Десятки–сотні мс (JIT-розігрів; залежить від обсягу коду) | Одиниці–десятки мс |
| Пам'ять | Більше (CLR, JIT-інфраструктура) | Значно менше |
| Розмір артефакту | Мала збірка + великий runtime | Один великий файл |
| Рефлексія | Повна підтримка | Обмежена (потрібні Source Generators) |
| Динамічний код | Assembly.LoadFrom, Reflection.Emit |
Не підтримується; Activator.CreateInstance — лише для типів, відомих під час збирання |
| Платформа | Кросплатформово (одна збірка) | Окремий бінарний для кожної ОС/CPU |
Коли NativeAOT вигідний:
- Мікросервіси і serverless (AWS Lambda, Azure Functions): час холодного старту критичний
- CLI-утиліти: один файл без залежностей, зручно розповсюджувати
- IoT та вбудовані системи: обмежена пам'ять, відсутність .NET runtime
- Мобільні платформи (iOS): JIT заборонений платформою
Обмеження NativeAOT: NativeAOT не може компілювати код, що залежить від рефлексії з невідомими типами. Бібліотеки, що через рефлексію перебирають властивості й методи довільних типів під час виконання (класичні серіалізатори, DI-контейнери), потребують адаптації: компілятор AOT і тримінг видаляють код, використання якого не видно під час збирання. Саме тому JSON Source Generators (19.1) і інші генератори розроблені як AOT-сумісна альтернатива до рефлексивного підходу.
Підсумок
| Поняття | Суть |
|---|---|
| CIL | Проміжна мова; результат роботи компілятора C# |
| Збірка (.dll/.exe) | CIL + метадані; кросплатформовий артефакт розгортання |
| Метадані | Опис типів і методів; основа рефлексії та перевірки типів |
| JIT | Перетворює CIL у машинний код під час першого виклику методу |
| Кеш JIT | Скомпільований код зберігається; повторні виклики — без JIT |
| AOT | Альтернатива JIT: компіляція до запуску; нативний бінарний файл |
| NativeAOT | AOT-режим .NET 7+: один нативний файл, швидкий старт, без JIT і без встановленого runtime (runtime вбудовано у файл) |
C#-код проходить шлях: .cs → компілятор → CIL у збірці → JIT при виконанні → машинний код процесора. Завдяки цьому ланцюжку .NET поєднує зручність високорівневої мови, кросплатформовість і продуктивність, близьку до нативного коду. NativeAOT — альтернативний шлях для сценаріїв, де JIT-розігрів неприйнятний.