JIT ve AOT Farkı: C# Projesini Native AOT ile Derlemek
Yazar: Oğuzhan KARAGÜZEL ·
C# uygulamaları yıllarca hep aynı şekilde çalıştı: kod ara bir dile derlenir, uygulama çalışırken de JIT derleyicisi onu makine koduna çevirir. .NET 7 ile gelen Native AOT bunu değiştirdi. Artık bir C# projesi, üzerinde .NET kurulu olmayan bir makinede tek bir dosya olarak çalışabiliyor. Bu yazıda JIT ile AOT arasındaki farkı, .NET'teki üç derleme şeklini, bir projeyi adım adım Native AOT ile nasıl derleyeceğinizi ve AOT'un nerede işe yaramadığını kendi ölçümlerimle anlatacağım.
JIT: çalışırken derlemek
C# derleyicisi kodunuzu doğrudan makine koduna değil, platformdan bağımsız bir ara dile (IL) çevirir. .dll dosyalarınızın içinde duran şey bu IL. Uygulama çalıştığında .NET çalışma zamanı (CLR), her metodu ilk çağrıldığı anda JIT (Just-In-Time) derleyicisiyle o makinenin işlemcisine uygun makine koduna çevirir.
Güncel .NET sürümlerinde JIT bunu kademe kademe (tiered) yapıyor:
- Tier 0: Metot ilk çağrıldığında hızlıca ve az optimizasyonla derlenir. Amaç uygulamayı bir an önce ayağa kaldırmak.
- İzleme: Sık çağrılan metotlar takip edilir; hangi dalların çalıştığı, hangi tiplerin geldiği kaydedilir (dinamik PGO).
- Tier 1: Sık kullanılan metotlar arka planda, toplanan bu bilgilere göre çok daha iyi optimize edilerek yeniden derlenir.
Bu yaklaşımın güçlü yanı, kodun gerçek kullanıma göre optimize edilmesi: JIT, uygulamanızın o makinede nasıl çalıştığını görüp ona göre karar veriyor. Zayıf yanı ise başlangıç. Her metodun ilk çağrısında bir derleme maliyeti var, JIT'in kendisi de bellekte yer kaplıyor.
AOT: önceden derlemek
AOT (Ahead-Of-Time), derlemenin uygulama çalışmadan önce, yayın sırasında yapılması demek. .NET'te bunun iki şekli var:
- ReadyToRun: IL'in yanına önceden derlenmiş makine kodu eklenir. Uygulama yine normal .NET çalışma zamanında çalışır; JIT hâlâ oradadır ve sık kullanılan kodu daha iyi derlemek için devreye girer. Bir tür ön hazırlık diyebiliriz. Ayrıntılarını ReadyToRun ile Soğuk Başlangıç yazısında anlattım.
- Native AOT: Uygulamanın tamamı, kullandığı .NET kütüphaneleriyle birlikte tek bir çalıştırılabilir dosyaya derlenir. Çalışırken ne JIT vardır ne IL; makinede .NET kurulu olması gerekmez. Go ya da Rust ile derlenmiş bir programa benzer.
Üç yaklaşımın karşılaştırması
| Özellik | JIT | ReadyToRun | Native AOT |
|---|---|---|---|
| Başlangıç süresi | En yavaş | Daha hızlı (kodu bol uygulamalarda) | Çok hızlı |
| Bellek kullanımı | Yüksek | JIT'e yakın | Düşük |
| Uzun süreli en yüksek hız | Çok iyi (dinamik PGO) | Çok iyi (sık kullanılan kod yeniden derlenir) | İyi, ama çalışma sırasında toplanan bilgiyle optimize edilemez |
| Makinede .NET gerekir mi? | Evet | Evet | Hayır |
| Yansıma ve dinamik kod | Tam destek | Tam destek | Kısıtlı; derlerken görülmeyen kod yok sayılır |
| Platform | Her yerde aynı dosya | Platforma özel | Platforma özel, aynı işletim sisteminde derlenir |
Ölçtüm: başlangıç, bellek ve boyut
Farkı görmek için iki uç noktası olan küçük bir minimal API yazdım ve üç şekilde derledim. Her birini 15 kez başlatıp, uygulamayı başlattığım andan ilk başarılı HTTP cevabına kadar geçen süreyi ölçtüm. Ortam: Docker Desktop (WSL2) üzerinde 2 çekirdek ve 2 GB bellekle sınırladığım bir Linux konteyneri, .NET 10.
| Derleme | İlk yanıt (ortanca) | Bellek (RSS) | Yayın |
|---|---|---|---|
| JIT | 195 ms | 62 MB | 152 KB (+ sunucudaki .NET) |
| ReadyToRun | 195 ms | 63 MB | 168 KB (+ sunucudaki .NET) |
| Native AOT | 23 ms | 17 MB | 9,5 MB tek dosya, .NET gerekmez |
Native AOT ilk yanıtı yaklaşık 8 kat hızlandırdı ve belleği dörtte bire indirdi. ReadyToRun'un burada bir şey kazandırmamasına şaşırmayın; bu kadar küçük bir uygulamada çalışan kodun neredeyse tamamı zaten önceden derlenmiş ASP.NET çerçevesinden geliyor. Sürekli yük altında çalışan bir serviste saniyedeki en yüksek istek sayısını ise ölçmedim. Orada JIT'in çalışma sırasında yaptığı optimizasyonlar aradaki farkı kapatabilir, hatta öne bile geçebilir.
Native AOT ile derlemek: adım adım
1. Ön koşullar. Native AOT son adımda platformun kendi bağlayıcısını (linker) kullanıyor:
- Windows: Visual Studio'da "Desktop development with C++" iş yükü (MSVC derleme araçları).
- Linux:
clangvezlib1g-devpaketleri (Ubuntu/Debian).
Önemli bir kısıt var: Native AOT işletim sistemleri arasında çapraz derleme yapmıyor. Linux sunucunuz için Windows'ta derleme yapamazsınız; Linux'ta ya da bir Linux konteynerinde derlemeniz gerekiyor.
2. Projede AOT'u açın. Tek bir ayar yetiyor:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>PublishAot'u açınca AOT ile uyumsuz kodu yakalayan analizörler de devreye giriyor ve sorunları derleme sırasında uyarı olarak gösteriyor. InvariantGlobalization zorunlu değil; kültüre özel biçimlendirmeye ihtiyacınız yoksa dosyayı küçültüyor.
3. Kodu AOT'a uygun yazın. En çok karşılaşacağınız değişiklik JSON. System.Text.Json varsayılan olarak tipleri çalışma sırasında yansıma ile inceliyor. AOT'ta bunun yerine kaynak üretici (source generator) kullanılıyor:
using System.Text.Json.Serialization;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
var app = builder.Build();
Todo[] todos = [new(1, "Yazıyı bitir", false), new(2, "Kapağı hazırla", true)];
app.MapGet("/todos", () => todos);
app.MapGet("/todos/{id:int}", (int id) =>
todos.FirstOrDefault(t => t.Id == id) is { } todo ? Results.Ok(todo) : Results.NotFound());
app.Run();
public record Todo(int Id, string Title, bool Done);
[JsonSerializable(typeof(Todo[]))]
[JsonSerializable(typeof(Todo))]
internal partial class AppJsonContext : JsonSerializerContext;CreateSlimBuilder, AOT'a uymayan ya da çoğu serviste zaten gerekmeyen özellikleri dışarıda bırakan daha hafif bir başlangıç. Sıfırdan başlıyorsanız dotnet new webapiaot şablonu bu ayarların hepsiyle birlikte geliyor.
4. Yayınlayın. Hedef platformu belirterek:
dotnet publish -c Release -r linux-x64Çıktı klasöründe uzantısı olmayan tek bir çalıştırılabilir dosya ve hata ayıklama sembollerinin bulunduğu bir .dbg dosyası oluşuyor. Sunucuya sadece çalıştırılabilir dosyayı kopyalamanız yeterli.
5. Docker'da derleyin. Windows'ta çalışıp Linux sunucuya yayın yapıyorsanız en kolayı derlemeyi bir konteynerde yapmak. Çalıştırmak için de içinde .NET olmayan, çok küçük runtime-deps imajı yetiyor:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
RUN apt-get update && apt-get install -y --no-install-recommends clang zlib1g-dev
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 -o /app
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0
WORKDIR /app
COPY --from=build /app/Bench .
ENTRYPOINT ["./Bench"]Uyarıları neden ciddiye almalısınız: gerçek bir çökme
Native AOT, derleme sırasında görebildiği kodu derliyor. Çalışma sırasında yansımayla bulunan tipler, sonradan yüklenen kütüphaneler ya da çalışırken üretilen kod (Reflection.Emit) derleyicinin göremediği şeyler. Analizörler bu riskleri IL2026, IL3050 gibi uyarılarla bildiriyor. Bu uyarılar "belki sorun olur" demek değil, "çalışırken çökebilir" demek.
Bunu görmek için Razor görünümü ve EF Core kullanan küçük bir MVC uygulamasını Native AOT ile derledim. Derleme, MVC ve EF Core kütüphanelerinden gelen bir sürü uyarıyla birlikte başarıyla tamamlandı ve bir çalıştırılabilir dosya çıktı. Dosyayı çalıştırınca uygulama daha tek bir isteğe cevap veremeden çöktü:
warning IL2026: Microsoft.AspNetCore.Mvc.ModelBinding...: Using member '...' which has
'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.
warning IL3053: Assembly 'Microsoft.EntityFrameworkCore' produced AOT analysis warnings.
warning IL3053: Assembly 'Microsoft.AspNetCore.Mvc.Razor' produced AOT analysis warnings.
$ ./Shop
Unhandled exception. System.TypeLoadException: Could not resolve type
'Microsoft.AspNetCore.Mvc.ApplicationParts.ConsolidatedAssemblyApplicationPartFactory'
in assembly 'Microsoft.AspNetCore.Mvc.Razor'.MVC, uygulama parçalarını açılışta yansımayla, tipin adından buluyor. AOT derleyicisi bu tipe doğrudan bir referans görmediği için onu dosyaya hiç koymamış. Kural basit: AOT uyarısı veren bir projeyi yayına almayın. Uyarıyı gideremiyorsanız o proje Native AOT'a uygun değil demektir.
Native AOT nerede uygun, nerede değil?
Uygun olduğu yerler:
- Komut satırı araçları: anında açılıyor, kullanıcının makinesinde .NET olması gerekmiyor.
- Sunucusuz (serverless) fonksiyonlar ve kullanılmadığında sıfıra inen konteynerler: soğuk başlangıcı doğrudan kullanıcı hissediyor.
- Minimal API ya da gRPC ile yazılmış küçük mikroservisler: daha az bellek, daha küçük imaj, aynı sunucuda daha çok örnek.
Uygun olmadığı yerler (şimdilik):
- ASP.NET Core MVC, Razor Pages ve Blazor Server: yukarıdaki örnekte gördüğünüz gibi yansımaya dayanıyorlar.
- EF Core'a çok bağlı uygulamalar: AOT desteği sınırlı ve deneysel, önceden derlenmiş model ve sorgular istiyor.
- Eklenti yükleyen, çalışırken kod derleyen ya da yansımayı yoğun kullanan kütüphanelere dayanan uygulamalar.
Bu sitenin altyapısı Elevare de bu yüzden Native AOT yerine ReadyToRun kullanıyor. MVC, Razor ve EF Core üzerine kurulu bir uygulamada başlangıcı hızlandırmanın güvenli yolu bu.
Sonuç
JIT, kodu çalışırken ve gerçek kullanıma bakarak derliyor; esnek, uzun vadede de çok hızlı ama başlangıçta yavaş. ReadyToRun bu yavaşlığı azaltıyor ve JIT'in esnekliğini koruyor. Native AOT ise JIT'i tamamen ortadan kaldırıyor: benim ölçümümde ilk yanıt 195 ms'den 23 ms'ye, bellek 62 MB'tan 17 MB'a indi. Karşılığında yansımadan ve dinamik koddan vazgeçmeniz gerekiyor. Küçük servisler ve araçlar için Native AOT, MVC ve EF Core ağırlıklı uygulamalar için ReadyToRun doğru seçim. Hangisini seçerseniz seçin analizör uyarılarını ciddiye alın ve mutlaka ölçün.

