🚀

.NET NativeAOT

Performance Revolution

A Deep Dive into AOT vs ReadyToRun vs Regular .NET



AWS Lambda Benchmarks & Best Practices

😰 The Problem We're Solving

User Request

⬇️

[ 6.7 seconds waiting... ]

⬇️

Response


"Why is this so slow?" 🤔

  • Cold start times in serverless environments
  • Cost implications of slow startups
  • User experience impact
  • AWS Lambda, Azure Functions, Google Cloud Functions

📋 Agenda

  1. Understanding Execution Models
  2. Why NativeAOT Matters
  3. Performance Results (Real Benchmarks)
  4. Implementation Deep Dive
  5. Coding Patterns & Best Practices
  6. Migration Strategies
  7. When to Use What
  8. Q&A
⚙️

Understanding Execution Models

Three Ways to Run .NET

The Three Execution Models

Model Description
Regular .NET JIT compilation at runtime
ReadyToRun (R2R) Hybrid: precompiled + JIT fallback
NativeAOT Full ahead-of-time compilation

Each has its place in the ecosystem

Regular .NET - Traditional JIT

Application Start Flow:

1. Load IL assemblies

2. Initialize runtime (CLR) ⏱️ Slow

3. JIT compile on first call ⏱️ Slow

4. Execute native code

5. Tier 0 → Tier 1 optimization

IL = Intermediate Language | JIT = Just-In-Time

Regular .NET - Pros & Cons

✅ Advantages

  • Maximum flexibility
  • Reflection support
  • Dynamic loading
  • Smallest package size
  • Cross-platform IL
  • Fastest dev iteration

❌ Disadvantages

  • Highest cold start time
  • Unpredictable warm-up
  • Larger memory footprint
  • Runtime overhead

ReadyToRun (R2R) - Hybrid Approach

Application Start Flow:

1. Load R2R + IL assemblies

2. Initialize runtime (CLR) ⏱️ Still needed

3. Execute precompiled code ⚡ Fast

4. JIT only for generics/edge cases

Key Point: Ships BOTH IL and precompiled native images

ReadyToRun - Pros & Cons

✅ Advantages

  • 34% faster startup than Regular
  • Minimal code changes
  • Maintains .NET flexibility
  • JIT fallback available

❌ Disadvantages

  • Larger package (IL + native)
  • Still requires managed runtime
  • Platform-specific builds
  • Similar memory footprint

NativeAOT - Ahead-of-Time

Application Start Flow:

1. Execute native binary ⚡ Instant

2. No runtime initialization ⚡ Eliminated

3. No JIT compilation ⚡ Eliminated

4. Predictable performance ⚡ Consistent

🎯 Single native executable, no CLR needed!

NativeAOT - Pros & Cons

✅ Advantages

  • 7× faster cold start
  • 6× faster warm runs
  • 50% less memory
  • Predictable performance
  • Deploy .NET 9/10 today
  • No runtime dependency

❌ Disadvantages

  • Reflection limitations
  • No dynamic assembly loading
  • Larger package than Regular
  • Platform-specific binaries
  • Longer build times
  • Requires code discipline

Startup Flow Comparison

REGULAR .NET:

[Download] → [Extract] → [Init CLR][Load IL][JIT] → [Execute]


NATIVEAOT:

[Download] → [Extract] → [Execute]

AOT eliminates the two slowest steps! ⚡

💡

Why NativeAOT Matters

The Serverless Challenge

The Serverless Challenge

Traditional .NET cold starts include:

  1. ✅ Download package from S3
  2. ✅ Extract to execution environment
  3. Initialize .NET runtime (biggest cost)
  4. Load and JIT assemblies (second biggest)
  5. ✅ Execute your code

NativeAOT eliminates steps 3 & 4 entirely!

Key Benefits Summary

Faster Cold Start
6680ms → 940ms
Faster Warm Runs
91ms → 14ms
50%
Less Memory
93MB → 42MB
73%
Cost Savings
$9.80 → $2.60/mo

Deploy Latest .NET Today

AWS Lambda Managed Runtimes:

dotnet8 ✅ Available

dotnet9 ❌ Not yet

dotnet10 ❌ Not yet

With NativeAOT (provided.al2023):

.NET 8 ✅ Works

.NET 9 ✅ Works

.NET 10 ✅ Works

.NET 11+ ✅ Will work

Don't wait for AWS to support new runtimes!

📊

Performance Results

Real AWS Lambda Benchmarks

Test Methodology

ParameterValue
PlatformAWS Lambda
Runtimesdotnet8, provided.al2023
Memory512MB (Regular/R2R), 256MB (AOT)
Business LogicDynamoDB write operation
Cold Start TestInvoke after 10min inactivity
Warm Test100 rapid invocations

❄️ Cold Start Comparison

Regular .NET 8
6680ms
ReadyToRun .NET 8
4389ms
AOT .NET 8
1447ms
AOT .NET 9
1006ms
AOT .NET 10
951ms ⚡

7.1× faster (Regular → AOT .NET 10)

🔥 Warm Execution Comparison

ReadyToRun .NET 8
99ms
Regular .NET 8
91ms
AOT .NET 8
19ms
AOT .NET 10
17ms
AOT .NET 9
14ms ⚡

6.5× faster (Regular → AOT .NET 9)

💾 Memory Usage

ReadyToRun .NET 8
89-96 MB
Regular .NET 8
88-93 MB
AOT .NET 8
46-48 MB
AOT .NET 9
43-46 MB
AOT .NET 10
42-45 MB ⚡

52% less memory (Regular → AOT .NET 10)

📈 Detailed Results

Function .NET Cold Start Warm Avg Memory
Regular 8 6680 ms 91 ms 88-93 MB
ReadyToRun 8 4389 ms 99 ms 89-96 MB
AOT 8 1447 ms 19 ms 46-48 MB
AOT 9 1006 ms 14 ms ⚡ 43-46 MB
AOT 10 951 ms ⚡ 17 ms 42-45 MB ⚡

💰 Cost Implications

Monthly Cost @ 10M Requests

Mode Avg Duration Memory Cost
Regular 91 ms 512 MB $9.80
ReadyToRun 99 ms 512 MB $10.40
AOT .NET 9 14 ms 256 MB $2.60
AOT .NET 10 17 ms 256 MB $2.70

Savings: 73% lower cost with AOT! 💰

🔧

Implementation Deep Dive

Project Configuration

Project Configuration - Regular

<!-- LambdaRegularDemo.csproj -->
<PropertyGroup>
  <OutputType>Library</OutputType>
  <TargetFramework>net8.0</TargetFramework>
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
  <PublishAot>false</PublishAot>
  <PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>

OutputType = Library (uses Lambda runtime)

Project Configuration - ReadyToRun

<!-- LambdaReadyToRunDemo.csproj -->
<PropertyGroup>
  <OutputType>Library</OutputType>
  <TargetFramework>net8.0</TargetFramework>
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
  <PublishReadyToRun>true</PublishReadyToRun>
  <TrimMode>partial</TrimMode>
</PropertyGroup>

Just add PublishReadyToRun=true! One line change.

Project Configuration - NativeAOT

<!-- LambdaAOTDemo9.csproj -->
<PropertyGroup>
  <OutputType>Exe</OutputType> <!-- MUST be Exe -->
  <TargetFramework>net9.0</TargetFramework>
  <RuntimeIdentifiers>linux-x64</RuntimeIdentifiers>
  <PublishAot>true</PublishAot>
  <SelfContained>true</SelfContained>
  <StripSymbols>true</StripSymbols>
  <TrimMode>partial</TrimMode>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

Key: OutputType=Exe produces native executable

Build Process Comparison

Regular/R2R Build

dotnet publish -c Release -o ./publish
zip -r lambda.zip ./publish/*
# Output: Multiple DLLs

NativeAOT Build

dotnet publish -c Release -o ./publish
mv ./publish/LambdaAOTDemo9 ./publish/bootstrap
zip -r lambda.zip ./publish/bootstrap
# Output: Single 'bootstrap' binary

📦 Package Contents

Regular Package (1.37 MB):

├── LambdaRegularDemo.dll

├── Shared.dll

├── Amazon.Lambda.Core.dll

├── AWSSDK.DynamoDBv2.dll

├── ... (many more DLLs)

└── runtimeconfig.json

NativeAOT Package (5.56 MB):


└── bootstrap


Single native executable!

💻

Coding Patterns

Best Practices for AOT

⚠️ The Reflection Challenge

NativeAOT uses STATIC ANALYSIS at build time.

Dynamic reflection breaks this!


❌ PROBLEMATIC:

• Type.GetType("MyClass")

• Activator.CreateInstance(type)

• Assembly.Load("Plugin")

• JsonSerializer.Serialize(obj) // Default uses reflection!

✅ Solution: JSON Source Generation

❌ DON'T: Uses reflection

var json = JsonSerializer.Serialize(input);

✅ DO: Source-generated code

[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(Guid))]
public partial class AOTJsonContext 
    : JsonSerializerContext { }

// Usage:
var json = JsonSerializer.Serialize(input, 
    AOTJsonContext.Default.DictionaryStringString);

✅ Solution: Constructor Injection

// ✅ AOT-Friendly: Constructor Injection
public class Function
{
    private readonly IDynamoDBRepository _repo;

    public Function(IDynamoDBRepository repo)
    {
        _repo = repo;  // Type known at compile time
    }

    public async Task<Guid> FunctionHandler(
        Dictionary<string, string> input, 
        ILambdaContext context)
    {
        return await _repo.CreateAsync(cts.Token);
    }
}

✅ Solution: Explicit Service Registration

❌ DON'T: Assembly scanning

services.AddFromAssembly(
    typeof(Startup).Assembly);

✅ DO: Explicit registration

public void ConfigureServices(
    IServiceCollection services)
{
    services.AddSingleton<IAmazonDynamoDB, 
        AmazonDynamoDBClient>();
    services.AddSingleton<IDynamoDBRepository, 
        DynamoDbRepository>();
    services.AddTransient<Function>();
}

⚠️ Trimming Warnings

warning IL2026: Using member 'Type.GetType(string)' 
which has 'RequiresUnreferencedCodeAttribute' can break 
functionality when trimming application code.

Action Items:

  1. Enable trim analyzer: <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
  2. Fix ALL warnings before deploying
  3. Use [JsonSerializable] for serialization
  4. Use constructor injection
🚀

Migration Strategies

Step-by-Step Approach

Migration Path Overview

1
Assessment
2
Code Prep
3
Pilot
4
Rollout

Don't migrate everything at once. Start with non-critical functions.

Phase 1: Assessment

<PropertyGroup>
  <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>

Identify Blockers:

  • ❌ Reflection usage
  • ❌ Dynamic loading
  • ❌ Incompatible libraries
  • ❌ Entity Framework (limited support)

ReadyToRun as Stepping Stone

<!-- Quick win: 34% faster cold starts -->
<PropertyGroup>
  <PublishReadyToRun>true</PublishReadyToRun>
  <TrimMode>partial</TrimMode>
</PropertyGroup>

✅ Benefits

  • Minimal code changes
  • Tests trimming compatibility
  • 34% faster cold starts
  • Low risk
🎯

When to Use What

Decision Guide

Quick Decision Guide

Need cold start < 1s? → AOT

Traffic > 1M/month? → AOT

Budget-sensitive? → AOT

Complex reflection? → Regular or R2R

Plugin architecture? → Regular

Rapid prototyping? → Regular

Migration testing? → ReadyToRun

Use Case Matrix

Scenario Recommendation Why
User-facing APIs ✅ AOT 7× faster cold start
Event processors ✅ AOT Frequent cold starts
Scheduled tasks ✅ AOT Always cold start
High volume (>10M/mo) ✅ AOT 73% cost savings
Plugin systems ✅ Regular Dynamic loading needed
Heavy ORM (EF Core) ⚠️ Regular/R2R Not fully AOT-compatible
MVPs/Prototypes ✅ Regular Fastest iteration

Limitations Summary

Limitation Regular R2R AOT
Reflection ✅ Full ✅ Full ❌ Limited
Dynamic loading ✅ Yes ✅ Yes ❌ No
Entity Framework ✅ Full ✅ Full ⚠️ Partial
Build time ⚡ Fast ⚡ Fast 🐌 2-5× slower
Cold start Slow Medium Fast
🎯

Key Takeaways

Key Takeaways

  1. AOT delivers real gains: 7× cold start, 6× warm, 50% memory
  2. Deploy .NET 9/10 today on Lambda via custom runtime
  3. AOT requires discipline: Source generators, constructor injection
  4. Start preparing now: Adopt patterns even on Regular
  5. R2R is viable middle ground: 34% improvement, low risk
  6. Measure your workload: Results vary by application
  7. The future is AOT: Each .NET version improves further

Quick Wins to Start Today

// Even if staying on Regular .NET:

// 1. Adopt JSON source generation
[JsonSerializable(typeof(MyModel))]
public partial class AppJsonContext : JsonSerializerContext { }

// 2. Use constructor injection
public MyService(IRepository repo) => _repo = repo;

// 3. Enable trim analyzers
// <EnableTrimAnalyzer>true</EnableTrimAnalyzer>

// 4. Avoid reflection patterns
// No Type.GetType(), Assembly.Load()

This makes future AOT migration trivial!

📚 Resources

Microsoft Documentation:

AWS Documentation:

This Repository:

  • github.com/whitewAw/dotnet-lambda-aot-performance-comparison

Questions?


Let's discuss!



GitHub: github.com/whitewAw

Repository: dotnet-lambda-aot-performance-comparison

🙏

Thank You!


Cold Start
Warm Runs
50%
Less Memory
73%
Cost Savings

Go Native! 🚀