ReadyToRun & Tiered Compilation
Understand ReadyToRun pre-compilation, tiered JIT compilation, and PGO (profile-guided optimization) in .NET.
ReadyToRun & Tiered Compilation is a free C# Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
JIT, R2R, and Tiered Compilation
.NET uses several compilation strategies to balance startup speed with peak throughput. Understanding JIT, ReadyToRun, and Tiered Compilation helps you choose the right settings for your workload.
How the JIT Works
The Just-in-Time (JIT) compiler converts IL bytecode to native machine code the first time each method is called. This adds latency to the first invocation but produces highly optimized code for the running machine.
// What happens at runtime:
// 1. App starts — methods are stubs pointing to the JIT
// 2. First call to MyMethod() → JIT compiles IL → native x64 code
// 3. Subsequent calls → native code runs directly (no JIT overhead)
// Startup cost: each method's first call takes microseconds for JIT
// Peak performance: excellent — JIT knows the actual CPU features
// .NET 9 JIT optimizations applied automatically:
// - Inlining
// - Loop unrolling
// - Escape analysis
// - SIMD vectorization
// - Profile-Guided Optimization (PGO)ReadyToRun (R2R) Compilation
ReadyToRun (R2R) pre-compiles your assemblies to native code at publish time. The app starts faster because JIT work is already done, at the cost of a larger on-disk size.
// Enable R2R in .csproj:
<PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
<!-- Optional: composite R2R — whole app in one native image -->
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
</PropertyGroup>
// Publish:
dotnet publish -c Release -r linux-x64 --self-contained
// Benefits:
// - Startup time improvement: ~20-30% faster than JIT-only
// - No runtime required (self-contained)
// Trade-off:
// - Larger binary (IL + R2R code both stored)
// - Publish takes longer (cross-gen2 runs at publish time)R2R vs Native AOT
R2R still ships IL alongside the native code and still requires the .NET runtime. Native AOT removes IL and the runtime entirely. Choose based on your deployment requirements.
// ReadyToRun:
// ✓ Still ships IL (fallback JIT for dynamically-generated code)
// ✓ Supports full reflection
// ✓ .NET runtime required on target
// ✓ Startup: ~30% faster than JIT
// Size: medium (IL + native)
// Native AOT:
// ✓ No IL, no runtime needed on target
// ✓ Startup: ~36x faster than JIT
// ✗ No reflection (must use source generators)
// ✗ Trimming limitations
// Size: smallest
// JIT only:
// ✓ Full reflection, no limitations
// ✓ Best peak throughput via PGO
// ✗ Slowest startupTiered Compilation
Tiered Compilation is enabled by default in .NET. Methods start at Tier 0 (quick compile) and hot methods are re-compiled at Tier 1 (full optimization) in the background.
// Tiered compilation lifecycle:
// Tier 0: fast JIT with minimal optimization
// → method is callable in ~100 microseconds
// After ~30 calls (configurable): marked as "hot"
// Tier 1: full JIT with all optimizations + PGO
// → background thread re-compiles the method
// → future calls use optimized native code
// Usually transparent — you don't need to configure it
// Disable (rarely needed, only for debugging JIT issues):
// DOTNET_TieredCompilation=0
// Disable tiered PGO if benchmarking:
// DOTNET_TieredPGO=0Profile-Guided Optimization (PGO)
.NET 8+ includes Dynamic PGO by default. The runtime collects call profile data at Tier 0, then uses that profile to generate highly specialized Tier 1 code for hot paths.
// PGO automatically specializes code like:
// Before PGO (generic virtual dispatch):
void Process(IAnimal animal) => animal.Speak();
// → runtime checks type on every call
// After PGO (type-specialized inline):
void Process(IAnimal animal)
{
if (animal is Dog) { /* inlined Dog.Speak() */ }
else if (animal is Cat){ /* inlined Cat.Speak() */ }
else animal.Speak(); // slow path
}
// Enabled by default in .NET 8+
// Verify:
// DOTNET_ReadyToRun=0
// DOTNET_TieredCompilation=1 (default)
// DOTNET_TieredPGO=1 (default .NET 8+)Startup Hooks
Startup hooks (DOTNET_STARTUP_HOOKS) let you inject code before Main runs, useful for diagnostics, configuration overrides, or instrumentation without modifying application code.
// Create a hook class:
public class StartupHook
{
public static void Initialize()
{
Console.WriteLine("[StartupHook] Initializing before Main");
// e.g., register telemetry, inject config overrides
Environment.SetEnvironmentVariable("FEATURE_FLAG", "1");
}
}
// Compile to a DLL and set the env var:
export DOTNET_STARTUP_HOOKS=/path/to/MyHook.dll
// Then run your app normally:
dotnet run
// [StartupHook] Initializing before Main
// [Your App Output]Runtime Configuration Knobs
The .NET runtime exposes environment variables and runtimeconfig.json knobs to tune GC, thread pool, and JIT behavior without code changes.
// runtimeconfig.template.json (baked into app at publish):
{
"configProperties": {
"System.GC.Server": true, // server GC (multi-core)
"System.GC.Concurrent": true, // background GC
"System.GC.HeapHardLimit": 536870912, // 512 MB hard limit
"System.Threading.ThreadPool.MinThreads": 20,
"System.Threading.ThreadPool.MaxThreads": 200,
"System.Runtime.TieredCompilation": true
}
}
// Environment variable overrides (useful in containers):
// DOTNET_GCHeapHardLimit=536870912
// DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=70
// DOTNET_GCConserve=1 // reduce memory at cost of throughputCrossgen2 and Composite Images
Crossgen2 is the tool that produces R2R images. Composite R2R pre-compiles an entire application (app + framework) into a single shared native image for the best possible startup.
// Composite R2R: compile app + framework into one native image
<PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
</PropertyGroup>
// Normal R2R includes multiple images per assembly
// Composite merges them → fewer file reads at startup
// Manual crossgen2 invocation:
dotnet tool install -g dotnet-crossgen2
crossgen2 \
--composite \
--out composite.dll \
MyApp.dll framework/System.Runtime.dll
// Inspect R2R content:
dotnet-ildasm --methodheader MyApp.dll | grep --color R2RReal-World: Tuning ASP.NET Core Startup
A checklist of practical settings for maximizing startup performance in a production ASP.NET Core service.
// .csproj:
<PublishReadyToRun>true</PublishReadyToRun> // pre-JIT at publish
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
<TieredCompilation>true</TieredCompilation> // default
// Program.cs — avoid startup work:
// ✓ Use AddHealthChecks() NOT AddDbContextCheck() for faster probe
// ✓ Lazy-initialize expensive singletons
// ✓ Don't await long tasks in Program.cs before app.Run()
// Container settings:
// DOTNET_GCServer=1 → server GC for multi-core pods
// DOTNET_GCHeapHardLimit=536870912 → prevent OOM in constrained pods
// DOTNET_ThreadPool_MinThreads=20 → reduce thread starvation under burst load
// Measure startup:
// app.MapGet("/startup-time",
// () => $"Ready in: {Stopwatch.GetElapsedTime(startTimestamp).TotalMs}ms");Quick Check
What is the key difference between ReadyToRun (R2R) and Native AOT?
Recap: ReadyToRun & Tiered Compilation
Key takeaways:
- Tiered Compilation: default in .NET — Tier 0 (fast compile) → Tier 1 (optimized + PGO)
- Dynamic PGO: .NET 8+ default — runtime profiles hot paths and specializes Tier 1 code
- ReadyToRun: pre-compiles at publish; ~30% faster startup; still needs runtime; supports reflection
- Native AOT: no IL, no runtime; ~36x faster startup; no reflection; smallest binary
- Tune via
runtimeconfig.jsonand environment variables (GC, thread pool, JIT knobs) - Composite R2R merges app + framework into one native image for best startup
Frequently asked questions
Is the “ReadyToRun & Tiered Compilation” lesson free?
Yes — the full text of “ReadyToRun & Tiered Compilation” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “ReadyToRun & Tiered Compilation”?
Understand ReadyToRun pre-compilation, tiered JIT compilation, and PGO (profile-guided optimization) in .NET. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “ReadyToRun & Tiered Compilation” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Native AOT Compilation
- Trimming & Reflection Limitations
- ReadyToRun & Tiered Compilation
- Benchmarking with BenchmarkDotNet