OOP Course
Сьогодні

Підрозділ 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 компілює методи під час виконанняЯк 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 та AOT компіляціїПорівняння JIT та 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-розігрів неприйнятний.

Розроблено Tomka Yurii · © 2026 ·