Mert Özen Her Satırda Daha İleri
Bilgisayar Bilimi Temelleri Compiler'lar ve Programlama Dilleri — #06

AOT Compilation: ReadyToRun, Native AOT ve Startup ile Throughput Arasındaki Seçim

Mert Özen 15 Kas 2026 13 dk 7 görüntülenme
AOT Compilation: ReadyToRun, Native AOT ve Startup ile Throughput Arasındaki Seçim

Native AOT, makine kodunu publish sırasında üretip startup'ı ve belleği belirgin şekilde azaltır; bedeli trimming, dynamic code yasağı, platforma özel build ve Dynamic PGO'nun yokluğudur. Aynı Bookstore API'sini JIT, ReadyToRun ve Native AOT ile publish edip startup, bellek ve throughput ölçüyor; filtre dilimizin AOT'de verdiği uyarıları, uyarı vermeden 500 dönen trimming hatasını ve Expression.Compile()'ın interpreter'a dönmesinin bedelini gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.404, runtime 10.0.15) ve ASP.NET Core 10 ile derlenip xUnit v3 4.0.3 testleriyle doğrulanmıştır; ölçümlerde BenchmarkDotNet 0.15.8 kullanılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için küçük bir API yazıyoruz: /books kataloğu JSON olarak döndürüyor, /books?filter=... ise seri boyunca yazdığımız filtre dilini kullanıyor. Aynı kodu üç şekilde publish edeceğiz: klasik JIT (just-in-time; uygulama çalışırken makine kodu üreten compiler), ReadyToRun ve Native AOT (ahead-of-time; makine kodunu publish sırasında üretmek).

Birinci soru: Startup süresinde Native AOT'nin JIT'e karşı ne kadar önde olmasını beklersin? ReadyToRun bu ikisinin neresine düşer?

İkinci soru: Filtre endpoint'i her istekte filtreyi parse edip bir expression tree'ye çeviriyor ve Compile() çağırıyor. Native AOT'de JIT yok; bu endpoint saniyede kaç istek karşılar, JIT'li sürümden daha mı yavaş olur?

AOT'nin Vaadi ve Bedeli

05. yazıda makine kodu program çalışırken, method method ve kademeli olarak üretiliyordu. AOT bu işi build'e, uygulama çalışmadan önceye taşıyor. Vaadi üç başlıkta toplanıyor: process açılır açılmaz hazır makine kodu çalıştığı için startup kısalıyor; JIT'in kendisi, IL (Intermediate Language; .NET'in işlemciden bağımsız ara kodu) ve onun metadata'sı bellekte durmadığı için bellek azalıyor; Native AOT'de çıktı tek bir executable ve hedef makinede .NET runtime'ının kurulu olması gerekmiyor.

Bedeli de üç başlıkta. Birincisi trimming: Native AOT yalnız compiler'ın ulaşılabilir gördüğü kodu çıktıya koyuyor; reflection ile çalışma anında adıyla bulunan bir member, hiçbir yerden referans almıyorsa çıktıda yok. Reflection'a dayanan serializer, mapper ve DI (dependency injection; bağımlılıkları class'a dışarıdan veren altyapı) kütüphaneleri bu yüzden sorun çıkarabiliyor. İkincisi dynamic code: System.Reflection.Emit ile çalışma anında IL üretip JIT'e vermek mümkün değil, çünkü JIT yok. Üçüncüsü platform: çıktı tek bir işletim sistemi ve mimari için. Windows x64 executable'ı Windows'ta, Linux executable'ı Linux'ta üretiliyor. Bir de daha az görünen bir bedel var: 05. yazıdaki Dynamic PGO (profile-guided optimization; çalışma anında toplanan profile göre optimizasyon) ve tiered compilation yok. Kod build'de bir kez optimize ediliyor; çalışma anındaki profile göre yeniden derlenmiyor.

ReadyToRun bu iki uç arasında bir hibrit. Assembly'nin içine IL'in yanında önceden derlenmiş makine kodu konuyor; runtime ve JIT yine orada. Microsoft Learn'deki ReadyToRun sayfasına göre bu kod JIT'in Tier1 kodu kadar optimize değil, bu yüzden tiered compilation sık çalışan ReadyToRun method'larını JIT'in ürettiği kodla değiştiriyor. 05. yazının figüründeki "ReadyToRun kodu → Tier1 instrumented → Tier1" yolu buydu. Kısıtlama yok: reflection, Emit, her şey çalışıyor.

Aynı API, Üç Publish

API, ASP.NET Core'un minimal API'si ve CreateSlimBuilder ile yazıldı. JSON için reflection yerine source generator (ApiJsonContext) kullanıyoruz; nedenine Tuzaklar bölümünde döneceğiz:

using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Bookstore.Catalog;
using Bookstore.Filter;

WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, ApiJsonContext.Default));

WebApplication app = builder.Build();

app.MapGet("/books", (string? filter) =>
{
    if (string.IsNullOrWhiteSpace(filter))
        return Results.Ok(SampleBooks.All);

    CompiledFilter compiled = FilterCompiler.Compile(filter);
    if (!compiled.Success)
        return Results.BadRequest(compiled.Diagnostics.Select(d => d.ToString()).ToArray());

    Func<Book, bool> predicate = compiled.Expression!.Compile();
    return Results.Ok(SampleBooks.All.Where(predicate).ToArray());
});

app.MapGet("/runtime", () => new RuntimeInfo(
    RuntimeFeature.IsDynamicCodeSupported,
    RuntimeFeature.IsDynamicCodeCompiled,
    System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription));

app.Run();

public sealed record RuntimeInfo(bool DynamicCodeSupported, bool DynamicCodeCompiled, string Framework);

[JsonSerializable(typeof(IReadOnlyList<Book>))]
[JsonSerializable(typeof(Book[]))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(RuntimeInfo))]
internal sealed partial class ApiJsonContext : JsonSerializerContext;

Üç publish komutu aynı projeyi kullanıyor; yalnız MSBuild property'leri farklı:

$ dotnet publish samples/Bookstore.Api -c Release -o publish/jit
$ dotnet publish samples/Bookstore.Api -c Release -p:PublishReadyToRun=true -o publish/r2r
$ dotnet publish samples/Bookstore.Api -c Release -p:PublishAot=true -o publish/aot
$ du -sh publish/jit publish/r2r publish/aot/Bookstore.Api
296K	publish/jit
436K	publish/r2r
9.6M	publish/aot/Bookstore.Api

-r vermediğimiz için SDK, ReadyToRun ve Native AOT çıktısını çalıştığı makinenin platformu için üretiyor; Windows'ta executable Bookstore.Api.exe olur ve boyutlar işletim sistemine göre biraz farklı çıkar. Boyutlar aynı şeyi ölçmüyor: JIT ve ReadyToRun klasörleri framework-dependent, yani makinede kurulu .NET runtime'ını kullanıyor. Native AOT executable'ı ise runtime'ı, GC'yi (garbage collector; kullanılmayan object'leri toplayıp belleği geri alan bileşen) ve kullandığımız framework kodunu içinde taşıyor. Debug sembolleri ayrı bir dosyada duruyor (Windows'ta .pdb); crash dump incelemek için onları saklamak gerekiyor.

ReadyToRun'un etkisini görmek için iki sürümü başlatıp tek bir istek gönderelim ve JIT'in kaç method derlediğini sayalım:

$ DOTNET_JitStdOutFile=jit-jit.txt DOTNET_JitDisasmSummary=1 ASPNETCORE_URLS=http://127.0.0.1:52506 publish/jit/Bookstore.Api > /dev/null &
$ sleep 1; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:52506/books; kill %1
200
$ grep -c 'JIT compiled' jit-jit.txt
1138
$ grep 'JIT compiled' jit-jit.txt | grep -c -E 'Bookstore|Program'
14
$ DOTNET_JitStdOutFile=jit-r2r.txt DOTNET_JitDisasmSummary=1 ASPNETCORE_URLS=http://127.0.0.1:52506 publish/r2r/Bookstore.Api > /dev/null &
$ sleep 1; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:52506/books; kill %1
200
$ grep -c 'JIT compiled' jit-r2r.txt
1090
$ grep 'JIT compiled' jit-r2r.txt | grep -c -E 'Bookstore|Program'
0

ReadyToRun, Bookstore'a ait 14 method'u JIT'ten kurtardı, ama JIT yine bine yakın method derledi. Bunların çoğu ASP.NET Core'un ve BCL'in (Base Class Library; .NET'in temel kütüphaneleri) generic instantiation'ları ve çalışma anında üretilen kodlar; framework'ün kendisi zaten ReadyToRun olarak geliyor. Learn'deki sayfa da az kodlu uygulamalarda ReadyToRun'dan belirgin bir kazanç beklenmemesi gerektiğini söylüyor.

Ölçüm: Startup, Bellek ve Throughput

Ölçüm için küçük bir araç yazdım: StartupProbe, verilen executable'ı başlatıyor ve ilk başarılı /books cevabına kadar geçen süreyi ölçüyor. Bunu beş kez tekrarlayıp medyanı alıyor; son çalıştırmada working set'i (process'in fiziksel bellekte tuttuğu sayfalar) okuyor ve iki endpoint'e 32 eş zamanlı istemciyle beşer saniye yük veriyor:

static async Task<double> WaitForFirstResponse(HttpClient client)
{
    long start = Stopwatch.GetTimestamp();
    while (true)
    {
        try
        {
            using HttpResponseMessage response = await client.GetAsync("/books");
            if (response.IsSuccessStatusCode)
                return Stopwatch.GetElapsedTime(start).TotalMilliseconds;
        }
        catch (HttpRequestException e) when (e.InnerException is SocketException)
        {
            // sunucu henüz dinlemiyor
        }
        await Task.Delay(2);
    }
}
$ dotnet build -c Release samples/StartupProbe
$ dotnet samples/StartupProbe/bin/Release/net10.0/StartupProbe.dll 5 publish/jit/Bookstore.Api
startup median    134 ms | idle   83 MB | /books  121861 rps | filter   8019 rps | after load  259 MB
$ dotnet samples/StartupProbe/bin/Release/net10.0/StartupProbe.dll 5 publish/r2r/Bookstore.Api
startup median    138 ms | idle   83 MB | /books  106552 rps | filter   7768 rps | after load  266 MB
$ dotnet samples/StartupProbe/bin/Release/net10.0/StartupProbe.dll 5 publish/aot/Bookstore.Api
startup median     68 ms | idle   25 MB | /books  113568 rps | filter 118373 rps | after load  124 MB

Yük üreticisi ve sunucu aynı makinede çalıştı; mutlak sayılar makineye göre değişir, karşılaştırılması gereken oranlar. Ölçümü üç kez tekrarladım: Native AOT'nin startup medyanı 49 ile 68 ms, JIT'inki 122 ile 142 ms arasında kaldı; throughput sayıları da yüzde 20'ye kadar oynadı. Bu yüzden JIT ile ReadyToRun arasındaki farkları ölçüm hatası sayıyorum.

Birinci tahminin cevabı: Native AOT ilk cevabı JIT'in yarısından da kısa sürede veriyor ve boşta üçte bir bellekle duruyor. ReadyToRun ise bu küçük uygulamada JIT'ten ayırt edilemiyor; az önce saydığımız JIT derlemeleri bunun nedeni. Düz /books endpoint'inde üçü de aynı aralıkta: JSON source generator'ın ürettiği kod üç yolda da aynı işi yapıyor.

İkinci tahminin cevabı şaşırtıcı: filtre endpoint'inde Native AOT, JIT'ten on kattan fazla istek karşılıyor. Nedenini filtre dilini Native AOT'ye taşıdıktan sonra göreceğiz.

Filtre Dili Native AOT'de: Önce Uyarılar

Bu ölçümü almadan önce filtre dilini Native AOT'ye hazırlamak gerekti. 04. yazıdaki code generator ilk publish'te build'i durdurdu (yollar kısaltıldı):

$ dotnet publish samples/Bookstore.Api -c Release -p:PublishAot=true -o publish/aot
...
src/Bookstore.Filter/CodeGen/ExpressionCompiler.cs(71,50): error IL3050: Using member 'System.Type.MakeGenericType(params Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime. [src/Bookstore.Filter/Bookstore.Filter.csproj]
src/Bookstore.Filter/CodeGen/ExpressionCompiler.cs(72,16): error IL2026: Using member 'System.Linq.Expressions.Expression.Field(Expression, String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed. [src/Bookstore.Filter/Bookstore.Filter.csproj]

İki uyarı da parametreli literal'ları üreten satırlardan. MakeGenericType, Captured<T>'yi çalışma anında bir tip argümanıyla kuruyordu. Native AOT generic tiplerin makine kodunu build'de üretmek zorunda; çalışma anında ortaya çıkan bir Captured<decimal> için kod olmayabilir (IL3050). Expression.Field(..., "value") ise field'ı adıyla, reflection ile arıyordu; trimmer o field'ın kullanıldığını göremez (IL2026). Uyarılar error olarak geldi, çünkü projede TreatWarningsAsErrors açık. Çözüm, bütün tipleri ve member'ları derleme anında görünür yazmak:

// her holder tipi açıkça yazılı; Native AOT her Captured<T>'yi derleme anında biliyor
return literal.Value switch
{
    int value => Captured<int>.Read(value),
    decimal value => Captured<decimal>.Read(value),
    string value => Captured<string>.Read(value),
    _ => Captured<bool>.Read((bool)literal.Value),
};

// C# compiler'ının yakalanan bir local için ürettiği şeklin aynısı: bir closure object'inde field okuma
private sealed class Captured<T>(T value)
{
    private static readonly FieldInfo ValueField = typeof(Captured<T>).GetField("value")!;

    // field adı SQL parametresinin adı olur (@value, @value1, ...)
    public readonly T value = value;

    public static MemberExpression Read(T value) => Expression.Field(Expression.Constant(new Captured<T>(value)), ValueField);
}

typeof(Captured<T>).GetField("value") trimmer'ın analiz edebildiği bir çağrı: tip ve isim sabit. Bu uyarıları yalnız publish'te değil her build'de görmek için library'ye bir satır ekledik:

<PropertyGroup>
    <RootNamespace>Bookstore.Filter</RootNamespace>
    <!-- trim ve AOT analyzer'ları yalnız Native AOT publish'te değil, her build'de çalışır -->
    <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

Uyarı Yoksa Sorun da Yok mu?

İkinci publish uyarısız bitti. İlk filtreli istek ise 500 döndü:

$ ASPNETCORE_URLS=http://127.0.0.1:52506 publish/aot/Bookstore.Api > aot.log &
$ curl -s -i 'http://127.0.0.1:52506/books?filter=price%20%3C%20300' | head -1
HTTP/1.1 500 Internal Server Error
$ grep -A5 'fail:' aot.log
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HNQ2OS56RLSN", Request id "0HNQ2OS56RLSN:00000001": An unhandled exception was thrown by the application.
      System.InvalidOperationException: The binary operator LessThan is not defined for the types 'System.Decimal' and 'System.Decimal'.
         at System.Linq.Expressions.Expression.GetUserDefinedBinaryOperatorOrThrow(ExpressionType, String, Expression, Expression, Boolean) + 0x148
         at Bookstore.Filter.CodeGen.ExpressionCompiler.EmitComparison(BoundComparison, ParameterExpression, LiteralMode) + 0xc4
         at Bookstore.Filter.CodeGen.ExpressionCompiler.Emit(BoundExpr, ParameterExpression, LiteralMode) + 0x190

decimal için < bir operatör method'u, decimal.op_LessThan. Expression.LessThan(left, right) onu çalışma anında reflection ile buluyor. JIT'li uygulamada sorun yok; Native AOT'de trimmer bu method'u hiçbir yerden çağrılmıyor gördüğü için çıkarmış ve publish bunu bir uyarıyla bildirmemiş. Çözüm yine aynı: method'u kendimiz isimlendirip expression'a vermek. C# compiler'ı da bir lambda'yı expression tree'ye çevirirken decimal karşılaştırmalarına operatör method'unu kendisi koyuyor:

// decimal ve string operatör method'larıyla karşılaştırılır; onları burada isimlendirmek Native AOT build'inde tutar
MethodInfo? method = left.Type == typeof(decimal) ? OperatorMethod(typeof(decimal), name)
    : left.Type == typeof(string) ? OperatorMethod(typeof(string), name)
    : null;
return Expression.MakeBinary(kind, left, right, liftToNull: false, method);

private static MethodInfo OperatorMethod(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, string name) =>
    type.GetMethod(name, [type, type])!;

[DynamicallyAccessedMembers] trimmer'a bir sözleşme veriyor: bu parametreye gelen tipin public method'ları korunmalı. Çağıran taraf typeof(decimal) ve typeof(string) geçtiği için trimmer hangi tiplerin method'larını tutacağını biliyor. Yeni testler her karşılaştırmanın operatör method'unu taşıdığını kontrol ediyor; Entity Framework Core'un (EF Core; .NET'in veritabanı erişim kütüphanesi) ürettiği SQL de değişmedi. Artık endpoint çalışıyor:

$ curl -s http://127.0.0.1:52506/runtime
{"dynamicCodeSupported":false,"dynamicCodeCompiled":false,"framework":".NET 10.0.15"}

Bu bölümden çıkan ders: trimming uyarıları gerekli ama yeterli değil. Reflection kullanan her kod yolu, Native AOT çıktısının kendisiyle çalıştırılarak test edilmeli.

Expression.Compile() Native AOT'de Interpreter'a Döner

/runtime çıktısındaki dynamicCodeSupported: false, Native AOT'de çalışma anında kod üretilemediğini söylüyor. Learn'deki Native AOT sayfası da System.Linq.Expressions'ın bu modda her zaman yorumlanan biçimini kullandığını yazıyor: Compile() IL üretmek yerine expression tree'yi gezen bir interpreter'a dönüyor. Aynı interpreter'ı JIT'li runtime'da Compile(preferInterpretation: true) ile seçebiliyoruz; iki yolu BenchmarkDotNet ile karşılaştıralım (çalıştırma benchmark'ları 1.000 kitap üzerinde):

$ dotnet run -c Release --project benchmarks/Bookstore.Filter.Benchmarks -- --filter '*CompileBenchmarks*'
...
| Method                | Mean         | Error       | StdDev      | Gen0    | Gen1   | Allocated |
|---------------------- |-------------:|------------:|------------:|--------:|-------:|----------:|
| CompileToIL           | 136,640.9 ns | 2,448.78 ns | 2,405.03 ns |  0.4883 | 0.2441 |    5777 B |
| CompileForInterpreter |     738.4 ns |    14.09 ns |    26.13 ns |  0.2728 | 0.0010 |    2288 B |
| RunCompiled           |   4,363.1 ns |    85.26 ns |   144.77 ns |       - |      - |         - |
| RunInterpreted        |  73,148.7 ns | 1,452.99 ns | 2,544.79 ns | 31.4941 |      - |  263984 B |

Tablo iki tahmini birlikte açıklıyor. IL'e derlemek pahalı: DynamicMethod üretiliyor ve JIT çağrılıyor, filtre başına yaklaşık 137 mikrosaniye. Interpreter için "derleme" ise neredeyse bedava. Buna karşılık çalıştırmada derlenmiş delegate yaklaşık 17 kat hızlı ve hiç allocation yapmıyor; interpreter 1.000 kitapta yaklaşık 264.000 byte ayırıyor. Bizim endpoint her istekte derleyip altı kitapta çalıştırdığı için derleme maliyeti baskın; JIT'li sürüm her istekte o maliyeti ödüyor. Native AOT burada daha hızlı, ama yanlış bir sebepten. Doğru çözüm 04. yazıda söylediğimiz gibi derlenmiş delegate'i filtre metnine göre cache'lemek; o zaman JIT'li sürüm hem derlemeyi bir kez yapıyor hem de en hızlı kodu çalıştırıyor.

"AOT" Kelimesinin Başka Anlamları

AOT bir teknik değil, bir zamanlama: derlemenin uygulama çalışmadan önce yapılması. Ne üretildiği bağlama göre değişiyor. ReadyToRun da Learn'e göre bir AOT biçimi, ama çıktısı IL ile yan yana duran makine kodu. Angular'ın AOT compiler'ı ise HTML template'lerini ve component'lerin TypeScript kodunu build sırasında JavaScript'e çeviriyor; çıktı makine kodu değil, tarayıcının JIT'lerinin yine derleyeceği JavaScript. Yeni Angular projelerinde ng build varsayılan olarak AOT ile çalışıyor.

TypeScript 7 ise başka bir yerden AOT'ye değiyor: compiler'ın kendisi Go ile yeniden yazıldı ve native bir executable olarak dağıtılıyor. Daha hızlı olan TypeScript'i derleyen araç; ürettiği JavaScript ve onu çalıştıran V8 (Chrome ve Node.js'in JavaScript engine'i) değişmedi. Ayrıntılar TypeScript İleri serisinin "TypeScript 7 ve Sonrası: Go ile Yazılan Derleyici ve Yeni Varsayılanlar" yazısında. Native AOT'nin ASP.NET Core tarafındaki desteği ve hangi özelliklerin çalışmadığı da ASP.NET Core Web API serisinin ".NET 8 ile ASP.NET Core Yenilikleri: Keyed DI, IExceptionHandler, Identity API ve Native AOT" yazısında.

Üretimde Dikkat

Native AOT build'i platforma özel ve kendi araç zincirini istiyor. Windows'ta Visual Studio'nun "Desktop development with C++" workload'u, Linux'ta clang gerekiyor. CI'da her hedef işletim sistemi ve mimari için ayrı bir job koşmak gerekiyor; Linux container'ı için executable'ı Linux'ta (ya da bir Linux build container'ında) üretirsin.

Kazancın en büyük olduğu yer, çok sayıda instance'ın sık başlayıp kapandığı ortamlar: sıfıra ölçeklenen servisler, kısa ömürlü job'lar, CLI araçları. Learn de faydanın en çok, çok sayıda instance'ı olan bulut servislerinde görüldüğünü söylüyor. Saatlerce açık kalan ve yoğun iş yapan bir API'de startup farkı bir kez ödeniyor; orada Dynamic PGO'nun kazandırdığı throughput daha değerli olabilir. Karar vermeden önce kendi uygulamanı üç şekilde publish edip yukarıdaki gibi ölç.

Bağımlılıkların da AOT uyumlu olması gerekiyor. Reflection'a dayanan kütüphaneler publish'te uyarı üretiyor. .NET 10 ile gelen VerifyReferenceAotCompatibility ayarı, AOT uyumlu olarak işaretlenmemiş referansları IL3058 uyarısıyla listeliyor.

Sık Düşülen Tuzaklar

Trimming uyarılarını bastırmak. #pragma warning disable IL2026 ya da SuppressTrimAnalysisWarnings build'i yeşile çevirir, ama uyarı çoğu zaman tek habercidir. Bastırılan uyarının karşılığı, üretimde ilk o kod yolundan geçen isteğin 500 dönmesi. Gördüğümüz gibi bazen uyarı bile gelmiyor; o yüzden Native AOT çıktısını entegrasyon testleriyle çalıştır.

Reflection tabanlı kütüphaneleri olduğu gibi kullanmak. System.Text.Json'ın reflection tabanlı serileştirmesi trimming ya da Native AOT açıkken varsayılan olarak kapalı; API'deki ApiJsonContext bu yüzden var. Source generator, serileştirme kodunu derleme anında ürettiği için trimmer neyin kullanıldığını görüyor. Aynı fikir Dapper'da Dapper.AOT ile uygulanıyor; Dapper serisinin "Dapper.AOT: Source Generator, Interceptor'lar ve Native AOT ile Dapper" yazısında anlatılıyor.

"AOT her zaman daha hızlı" varsayımı. AOT startup'ı ve belleği kesin olarak iyileştiriyor; peak throughput'u ise iyileştirmeyebilir. Dynamic PGO yok, expression tree'ler yorumlanıyor, bazı generic kodlar daha genel derleniyor. Filtre endpoint'inde AOT'nin on kattan fazla önde çıkması da AOT'nin değil, bizim her istekte derleme hatamızın sonucuydu.

Küçük Bir Deneme

★☆☆ Native AOT çıktısını -p:OptimizationPreference=Size ile yeniden publish et. Executable'ın boyutu ve StartupProbe'un sayıları nasıl değişti?

★★☆ API'de derlenmiş delegate'leri filtre metnine göre cache'le (boyutu sınırlı bir ConcurrentDictionary ya da IMemoryCache ile). StartupProbe'u üç çıktı için tekrar çalıştır; filtre endpoint'inde sıralama nasıl değişti?

★★★ OperatorMethod'daki [DynamicallyAccessedMembers] attribute'ünü kaldır ve hem dotnet build hem Native AOT publish çıktısına bak. Hangi uyarı geldi? Ardından RuntimeFeature.IsDynamicCodeSupported'a bakarak Native AOT'de interpreter, JIT'li runtime'da IL derlemesi kullanan bir CompileFilter helper'ı yaz ve iki yolu da test et.

Bu yazıda makine kodunun ne zaman üretildiğini değiştirdik. Bir sonraki yazıda runtime'ın diğer büyük parçasına geçiyoruz: garbage collector. Mark-sweep-compact, generation'lar, write barrier ve .NET'in Server GC'si ile DATAS'ını (Dynamic Adaptation To Application Sizes; heap sayısını yüke göre ayarlayan mod), yukarıdaki API'nin yük altında 260 MB civarına çıkan belleği üzerinden inceleyeceğiz.