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;
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:
- C# compiler stage – Only literals the source code can see.
-
JIT stage – Can fold
static readonly
values,Environment.ProcessorCount
, and anything exposed by inlining. - 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;
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
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();
.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;
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!
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)
- Higher hot-path throughput – Fewer instructions means better IPC and lower power draw.
- Unlocks other optimizations – When bounds checks fold away, the JIT can unroll loops it previously skipped.
- Safer refactors – Move code into helper methods without fearing that a constant will “escape” and become runtime overhead.
-
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)