DEV Community

Cover image for Constant Folding in .NET 10: Turning Dead Weight Into Pure Throughput
Sukhpinder Singh
Sukhpinder Singh

Posted on

Constant Folding in .NET 10: Turning Dead Weight Into Pure Throughput

I still remember the first time I looked at JIT-generated assembly and thought, Why on earth is the processor adding six and two inside a hot loop? It turned out the compiler hadn’t spotted the constants in my code. That one oversight ballooned into cache misses, battery drain, and eventually an expensive post-mortem. Since then I’ve had a standing rule: always check what your compiler can fold, and celebrate when it folds more.

.NET 10 gives us a lot to celebrate.


What constant folding really means

At its core, constant folding is nothing more than the compiler saying, “I’m smarter than doing this math a billion times; let me do it once now.” When you write:

int Add(int i) => i + 2 * 3;
Enter fullscreen mode Exit fullscreen mode

the C# compiler rewrites the IL so the multiplication happens at build time, not at run time. The generated method is effectively i + 6.

But compile time is a moving target:

  1. C# compiler stage – Only literals the source code can see.
  2. JIT stage – Can fold static readonly values, Environment.ProcessorCount, and anything exposed by inlining.
  3. Link-time trimming / AOT – May know even more, especially if dead code is removed.

Each step lets the runtime shave off more instructions, unlock dead-code elimination, and free up registers for the stuff that really matters—your business logic.


The classic math fold: beyond the C# compiler

Take the pair of methods below:

int M1(int i) => i + M2(2 * 3);
int M2(int j) => j * Environment.ProcessorCount;
Enter fullscreen mode Exit fullscreen mode

The C# compiler folds 2 * 3 into 6. Nice, but the bigger win is inlining. The JIT in .NET 9 can inline M2, notice that Environment.ProcessorCount is a constant on this machine (say 16), and produce:

lea eax, [rsi + 60h] ; i + 96   (0x60 = 96)
ret
Enter fullscreen mode Exit fullscreen mode

Four bytes of machine code, zero multiplications, zero memory loads.


Null-check folding: a practical win

Null checks are cheap—until you have two in a row inside every string.AsSpan() call:

s ??= "";
return s.AsSpan();
Enter fullscreen mode Exit fullscreen mode

.NET 9 emitted two test rsi, rsi instructions. .NET 10 folds the second away, dropping method size from 41 to 25 bytes. For a one-off call you’d never notice; inside a UTF-8 parser that runs millions of times a second, the branches and T-front-end pressure disappear.


Folding conditional expressions

Consider:

string tmp = condition ? GetOne() : GetTwo();
return tmp is not null;
Enter fullscreen mode Exit fullscreen mode

Because both helpers return hard-coded strings, the nullness is guaranteed. .NET 9 still materialized the variable and tested it. .NET 10 punts directly to mov eax, 1; ret—six bytes. The engine knew the answer before your code ran.


SIMD comparison folding

Vector logic is a performance superpower—right up until the JIT refuses to pre-compute obvious comparisons. Two PRs (#117099 and #117572) teach .NET 10’s JIT to fold more vector relations:

Vector128<int> mask = vec == Vector128<int>.Zero; // constant result? Fold it!
Enter fullscreen mode Exit fullscreen mode

That cuts register pressure, frees up cache bandwidth, and—counter-intuitively—makes auto-vectorization easier because the IR is simpler.


Why you should care (even if you never open a disassembler)

  1. Higher hot-path throughput – Fewer instructions means better IPC and lower power draw.
  2. Unlocks other optimizations – When bounds checks fold away, the JIT can unroll loops it previously skipped.
  3. Safer refactors – Move code into helper methods without fearing that a constant will “escape” and become runtime overhead.
  4. Cleaner APIs – You can write expressive guard clauses (value ?? throw) knowing the JIT often erases them.

Guidelines for “fold-friendly” code

  • Prefer static readonly over lazy singletons when the value is immutable; the JIT treats them as constants.
  • Keep helpers small and stateless—better inline odds equal better folding odds.
  • Mark trivial wrappers with [MethodImpl(MethodImplOptions.AggressiveInlining)] only when measurements prove it helps.
  • Use pattern matching (is null, is not null) freely; modern JITs see through them.
  • Rely on Vector128<T>.Count, IntPtr.Size, and similar “configuration constants”; .NET will fold them.

A dev-ops anecdote

Our logging pipeline builds interpolated strings in a tight loop. After moving to .NET 10 RC1, CPU usage on the ingestion service dropped by 8% without a single code change. Root cause? Null-check folding inside string.Create and the SIMD tweaks in IndexOfAny. The dev team got to brag, but the real hero was the JIT’s constant-prop engine.


Takeaways

Constant folding isn’t new, but the scope keeps expanding. .NET 10 folds more math, more null checks, and more vector ops than any release before it—while generating smaller, tighter machine code.

Next time your profiler shows an unexpected bump in a supposedly trivial method, peek at the assembly. If you spot a multiply by sixteen or a redundant null test, upgrade the runtime—or file an issue and watch the .NET team fold your constant in the next preview.

Top comments (0)