Start Debugging

The performance and Native AOT cheat sheet

Measure first, then pick the knob.

This site uses Walter, an AI agent, for research, writing, and maintenance. Editorial and verification process

This pillar collects everything on the site about making code faster and smaller - profiling before you guess, allocation and Span<T> work, caching, the JIT and compression changes in the .NET 11 cycle, and the constraints Native AOT puts on your code.

What to read first

Measure first: profiling with dotnet-trace and diagnosing a managed memory leak with gcdump and dump are the loops to learn. Before you tune anything, tiered compilation and PGO explain what the runtime already does for free - the newest example is the JIT devirtualizing generic virtual methods.

For allocation, what Span<T> actually speeds up leads and List vs Span vs ReadOnlySpan picks the type; ref T loops vs Span indexing prices the loop shape. Shipping smaller? Native AOT vs ReadyToRun vs JIT settles the fork, what Native AOT costs you is the honest ledger, and trim-safe code is the discipline; the failure you’ll hit first is reflection-based serialization disabled, and FluentValidation 12.2’s IsInEnum fix shows a library catching up. On the server, HybridCache vs IMemoryCache vs IDistributedCache and detecting N+1 queries remove more time than any micro-optimisation; when an expired MemoryCache entry leaves memory explains a growing cache, and a compiled model takes the model build off startup. In hot sync paths, waiting on a ValueTask without allocating keeps the zero-byte fast path.

JSON metadata and reusable options

For a small console example, disable reflection and serialize with a generated context. It reproduces the default failure and a working JSON round trip. When sharing configured options, provide a resolver before calling MakeReadOnly; that guide also demonstrates copying a frozen options instance before changing it.

What’s on this page

The list below auto-collects posts tagged with any of: performance, native-aot - which pulls in the Flutter and Dart performance posts too. Newest first.

Companion pillars: async and concurrency and the .NET 11 tracker.

Index (103 posts)

2026 / 11

  • How to do DistinctBy but keep the last element of each duplicate in C#, in source order

    Enumerable.DistinctBy keeps the first element per key. To keep the last one and still return results in the order they appear in the source, buffer the input, scan it backwards with a HashSet, and yield the survivors forwards. Eight implementations checked for order, null keys, double enumeration and argument validation on .NET 10.0.10 and .NET 11 RC3, and benchmarked on .NET 10.

  • Why LINQ ThenBy computes every secondary key, and how to compute them only on ties

    OrderBy(...).ThenBy(...) runs every key selector for every element before it compares anything, even when the OrderBy keys are already unique. Measured on .NET 10 and .NET 11 RC3: what the runtime does, why the popular lazy-key workaround still computes keys it never needs, and three patterns that call an expensive secondary key only for tied elements.

  • How to loop over two variables in C# without allocating an array

    foreach over (ReadOnlySpan<T>)[a, b] allocates nothing on .NET 8 and later and works for reference types, which stackalloc cannot do. On .NET 10 the compiler backs it with the BCL's InlineArray2<T>, and the JIT can even stack-allocate a plain new[] { a, b } once the method reaches Tier 1. Measured on .NET 10.0.10 and .NET 11 RC 3.

  • Is List<T>.Sort stable in C#? Why equal elements change order

    List<T>.Sort, Array.Sort, Span<T>.Sort and ImmutableArray.Sort are unstable introsorts, so items with equal keys can swap places. Enumerable.OrderBy is stable. Measured on .NET 10: why lists of 16 or fewer items look stable, why three items are not, how to get a stable in-place sort, and what it costs.

  • When is Stopwatch not high resolution in .NET, and what do IsHighResolution and Frequency report on each OS?

    On .NET Core and .NET 5+ Stopwatch.IsHighResolution is a hard-coded true and never false. Frequency is whatever QueryPerformanceFrequency says on Windows (usually 10 MHz) and a fixed 1,000,000,000 on Linux, macOS, iOS, Android and WASM, which is a unit, not a resolution. Only .NET Framework can fall back to DateTime. Source walkthrough plus a measured run on .NET 10 / Apple M4 showing 1 ns nominal ticks but 41.67 ns real steps.

  • What is [ModuleInitializer] in C# and when should a library use it?

    [ModuleInitializer] makes the C# compiler emit a <Module> static constructor that calls your method once per assembly. When it runs is looser than most people think: on the JIT it can fire before a branch that never executes, under Native AOT it runs eagerly at startup or not at all. Why CA2255 warns libraries off it, the few cases where a library should still use it, and measured behaviour on .NET 10.

2026 / 10

  • yield return iterator vs returning a List<T>: what an IEnumerable method actually costs in C#

    A yield return method costs one 40-byte state machine object per call no matter how many items it produces; a List<T> costs the whole backing array up front. Measured on .NET 10.0.10: iterator wins on memory and early exit, List<T> wins when the caller enumerates more than once, needs Count, or calls ToList().

  • ref T loop vs Span<T> indexing in C#: why the span version is as fast or faster in .NET 10

    On .NET 10 a plain for loop over span[i] bounded by span.Length compiles to a bounds-check-free, strength-reduced, down-counting loop. A hand-written Unsafe.Add(ref r, i) loop with an int index keeps a sign extension and an up-counting compare, and measured 2-5% slower. Benchmarks and JIT disassembly for arm64 and x64 on .NET 10.0.10.

  • Try pattern vs throwing exceptions for expected failures in C#

    Return bool from a TryX method when failure is an ordinary outcome the caller will branch on, and throw when it is a bug or a broken precondition. Measured on .NET 10.0.10: a failed int.TryParse costs 4.9 ns, a caught FormatException costs 2,487 ns.

  • How to iterate over two collections in lockstep without heap allocations in C#

    Enumerable.Zip allocates 144-208 bytes per call and runs about 10x slower than a plain loop. For arrays and lists, a for loop over two spans sliced to the same length is allocation-free and the fastest option; for foreach syntax, a small ref struct zip enumerator gets within 25% of it. Measured on .NET 10.0.10 and .NET 11 RC 3.

  • Lazy<T> vs LazyInitializer.EnsureInitialized vs double-checked locking in C#

    Use Lazy<T> by default: it runs the factory once, caches exceptions, and its reads are too cheap to measure after initialization. Use LazyInitializer.EnsureInitialized when you lazily fill many fields or array slots and cannot afford an extra object per value. Hand-written double-checked locking is almost never worth it on .NET 10.

  • What happens to an expired MemoryCache entry, and when is it actually removed from memory?

    An expired MemoryCache entry is not removed on a timer. It stays in the dictionary until a read touches it, a throttled background scan runs, or compaction kicks in. Here is exactly when each happens, and how to get eager eviction.

  • Thread.Sleep vs Task.Delay vs SpinWait for waiting in C#

    Use await Task.Delay for any timed wait in async code, Thread.Sleep only on a dedicated thread you own, and SpinWait only for sub-microsecond waits inside lock-free code. To wait for a condition, use a signal (ManualResetEventSlim, SemaphoreSlim, TaskCompletionSource) instead of any of the three. Measured on .NET 10.0.10.

  • FluentValidation 12.2 fixes IsInEnum() crashing native AOT apps on [Flags] enums

    FluentValidation 12.2.0, released October 10, 2026, swaps Enum.GetValues for Enum.GetValuesAsUnderlyingType inside EnumValidator, so IsInEnum() on a [Flags] enum no longer throws NotSupportedException in a native AOT binary.

  • What Is a Compiled Model in EF Core 11, and When Is It Worth Enabling?

    A compiled model is generated C# that recreates your EF Core model without running OnModelCreating. Measured on EF Core 11 RC1: model load drops from 714 ms to 215 ms at 500 entities, but the MSBuild route (which replaced EFOptimizeContext) is slower, stale models fail silently, and query filters block generation.

  • What Is Transparent Struct Layout, and Why Does a Single-Field Wrapper Struct Change the Calling Convention in .NET?

    A transparent struct is one the ABI treats exactly like its only field. .NET 11 has no such guarantee: a struct wrapping a double is passed in RCX on Windows x64 but in XMM0 on Linux and macOS. Here is the disassembly from the .NET 11 JIT on three ABIs, the P/Invoke bugs it causes, and how to write wrapper types that are safe at every boundary.

  • AddDbContextPool vs AddDbContextFactory for running EF Core queries in parallel

    AddDbContextPool hands out one scoped DbContext per DI scope, so it cannot run two queries at once. AddDbContextFactory and AddPooledDbContextFactory give you a context per call, which is what parallel queries need. Measured on EF Core 11 RC 1: the pooled factory creates a context in 342 ns and 40 B versus 17 us and 44 KB.

  • C# Dev Kit 11.0 Collapses Six Processes Into One Native AOT Binary

    C# Dev Kit 11.0 pre-release replaces six managed processes with a single Native AOT CSDevKit process and a persistent project cache. A 407-project Aspire solution is ready in 3.0s instead of 84.1s, using 316 MB instead of 2,072 MB.

  • How to wait for a ValueTask synchronously in a non-async method without allocating

    Check IsCompleted first and read the result through GetAwaiter().GetResult(), which costs 0 bytes. Only fall back to blocking when the ValueTask is still pending, and do that with a cached event and UnsafeOnCompleted instead of AsTask(), which allocates 72 to 208 bytes per call. Measured on .NET 10 and .NET 11 RC1.

  • EF.Parameter vs EF.Constant in EF Core 11 queries

    EF.Constant inlines a captured value as a SQL literal, EF.Parameter turns a literal into a SQL parameter. Keep EF Core's defaults, use EF.Parameter to stop dynamically built expression trees from recompiling on every call, and use EF.Constant only for a value with a handful of distinct values whose data is skewed enough to need its own plan.

  • Volatile.Read vs Volatile.ReadBarrier in .NET 10

    Volatile.Read is an acquire load of one location. Volatile.ReadBarrier, new in .NET 10, is a fence that gives every earlier read acquire semantics. Use Volatile.Read for flags and published references, and ReadBarrier when you need a batch of plain or non-atomic reads to complete before the next memory access, as in a seqlock.

2026 / 09

2026 / 08

2026 / 07

  • Fix: Reflection-based serialization has been disabled for this application

    This InvalidOperationException means PublishTrimmed or PublishAot flipped JsonSerializerIsReflectionEnabledByDefault to false. Fix it with a source-generated JsonSerializerContext.

  • How to build a Flutter web app with WebAssembly using flutter build web --wasm

    A complete guide to shipping a Flutter web app compiled to WebAssembly on Flutter 3.44: what the two emitted builds look like, why Firefox and Safari still get JavaScript because of the loader's wasmAllowList, migrating off dart:html for dart2wasm, the COOP/COEP headers that decide whether skwasm runs multi-threaded, and how to prove at runtime which build the browser actually loaded.

  • How to diagnose a managed memory leak with dotnet-gcdump and dotnet-dump

    A complete workflow for finding a managed memory leak in .NET 11: confirm growth with dotnet-counters, take two gcdumps and diff them, then collect a dump and use dumpheap, gcroot, and objsize in dotnet-dump analyze to find what is still holding the reference.

  • WebApplication.CreateBuilder vs CreateSlimBuilder vs CreateEmptyBuilder in ASP.NET Core 11

    Use CreateBuilder for a normal app, CreateSlimBuilder when you publish trimmed or Native AOT behind a TLS proxy, and CreateEmptyBuilder only when you want to register every service yourself. Here is the feature matrix and the gotchas that force the call.

  • Output caching vs response caching in ASP.NET Core 11: which should you use?

    Output caching is the right default for almost every server-side app in ASP.NET Core 11. Response caching only wins when your goal is to steer browser and proxy caches through HTTP headers. Here is the decision, with a feature matrix and the gotchas that force the call.

  • How to add response compression to an ASP.NET Core 11 API

    A complete guide to response compression in ASP.NET Core 11: AddResponseCompression and UseResponseCompression, the new built-in Zstandard provider alongside Brotli and Gzip, compression levels, EnableForHttps and the CRIME/BREACH risk, custom MIME types, middleware ordering, and when to let the reverse proxy do it instead.

  • How to add output caching to a minimal API in ASP.NET Core 11

    A complete, working guide to output caching in an ASP.NET Core 11 minimal API: AddOutputCache and UseOutputCache, CacheOutput on endpoints and MapGroup, named and base policies, Expire, VaryByQuery and VaryByHeader, tag-based eviction with EvictByTagAsync, cache stampede protection, ETag revalidation, and a Redis backing store.

  • .NET 11 Runtime Async Drops the EnablePreviewFeatures Flag

    As the .NET 11 previews progress toward the November release, Runtime Async has graduated: net11.0 projects opt in with a single MSBuild property, and the runtime libraries themselves now ship compiled on it.

  • What is PGO in .NET and do I need to opt in?

    PGO (profile-guided optimization) lets the .NET JIT specialize hot code for the types and branches your workload actually hits. Dynamic PGO has been on by default since .NET 8, so on .NET 8 and later you do not need to opt in. Here is what it does, how to see its effect, and the rare cases where you touch the knob.

  • What is tiered compilation and how do I reason about it?

    Tiered compilation lets the .NET JIT compile every method twice: once fast and unoptimized to get your app running, then again with full optimizations once the runtime knows a method is hot. Here is how tier 0, tier 1, on-stack replacement, and Dynamic PGO fit together, and how to observe and tune them.

  • What is trim-safe code and how do I write it?

    Trim-safe code is code the .NET trimmer can statically prove is reachable, so it survives when unused code is removed from a self-contained app. This is the practical guide: turn on the analyzer, drive every IL2xxx warning to zero, annotate reflection with DynamicallyAccessedMembers, propagate RequiresUnreferencedCode to public APIs, and replace unanalyzable patterns with source generators.

  • SkiaSharp 4.0 Ships Stable: 24% Faster GPU Rendering and a Cleaned-Up API

    SkiaSharp 4.148.0 is the first stable v4 release. GPU-heavy UIs render up to 24% faster, CPU shaders run ~6x faster, and the legacy API surface is finally retired. Here is what upgrading actually costs you.

2026 / 06

2026 / 05

2026 / 04

2026 / 02

2026 / 01

2025 / 04

2025 / 01

  • .NET Performance: ToList vs ToArray

    .NET 9 significantly improves ToArray performance using InlineArray, making it faster and more memory-efficient than ToList. See benchmarks comparing .NET 8 vs .NET 9.

All pillars Home