Deep dive into .NET 11 Performance Improvements #511/08/2026 | 4 minutes to read | Open in Markdown
In this series, I explore some of the most interesting .NET 11 performance improvements described in Stephen Toub's blog post, Performance Improvements in .NET 11.
Each post focuses on one example from the original article while digging a little deeper into an additional aspect that was not covered there. The series covers the following topics:
- Allocation free numbers
- Virtual generic method calls
- Vector zero equality
- Guid creation on Linux
- Current post: JIT generating implicit-else assembly instructions
- Guid equality
- Copy-on-write locking strategy
JIT generating implicit-else assembly instructions
In this optimization, the JIT improves if-conversion, which can allow an if-else statement to be represented as a conditional operation. This can be useful when a branch is difficult to predict: a misprediction can waste CPU cycles while the processor recovers. A conditional move can avoid a control flow branch, but it is not always faster; performance depends on the surrounding code and the target CPU. Consider this example:
private static int ImplicitElse(int left) { int count = 0; if (left < 1000) count += 1; else count += -1; return count; }
The ImplicitElse method is JIT-compiled to the following x64 assembly:
; Benchmarks.ImplicitElse(Int32)
mov eax,0FFFFFFFF
mov edx,1
cmp ecx,3E8
cmovl eax,edx
ret
; Total bytes of code 20
The cmp instruction compares left with 0x3E8 (1000 in decimal), and cmovl is a conditional move. The eax register initially holds -1, while edx holds 1. If the comparison indicates that left is less than 1000, cmovl copies the value in edx to eax; otherwise, eax remains -1.
Conditional moves can be useful when branch outcomes are hard to predict, such as when comparing random values. However, the JIT does not always preserve this code shape when the logic is placed in a loop. In my tests, a loop with three iterations was unrolled, producing three cmovl instructions. With ten iterations, the generated code changes:
private static int ImplicitElse(int left) { int count = 0; // Different assembly depending on the iteration limit: 1,3 and 10 for (int i = 0; i < 10; i++) { if (left < 1000) count += 1; else count += -1; } return count; }
The conditional move is replaced by conditional branches and jumps:
; Benchmarks.ImplicitElse(Int32)
xor eax,eax
mov edx,0A
M01_L00:
cmp ecx,3E8
jge short 0000000000002176
inc eax
M01_L01:
dec edx
jne short 0000000000002167
ret
M01_L02:
dec eax
jmp short 0000000000002171
; Total bytes of code 26
In this example, left < 1000 is loop-invariant, so the branch outcome remains the same on every iteration; the disassembly alone does not show that this version is slower.
This could be an opportunity for a future JIT improvement, but how much performance would replacing these branches actually gain? To explore that, I changed the comparison to use i instead of 1000:
private static int ImplicitElse(int left) { int count = 0; for (int i = 0; i < 10; i++) { if (left < i) count += 1; else count += -1; } return count; }
Next, I tried using arithmetic right shifting to extract the sign bit of the difference. The expression contributes +1 when left - i is negative and -1 otherwise:
private static int ImplicitElseNoInlined(int left)
{
int count = 0;
for (int i = 0; i < 10; i++)
count -= ((left - i) >> 31) | 1;
return count;
}
This is not completely equivalent: when
leftis close toint.MinValueandiis positive, the subtraction can overflow and produce the wrong sign. One can use acheckedblock for correctness, but this expression would still have narrower preconditions.
; Benchmarks.ImplicitElseNoInlined(Int32)
xor eax,eax
mov edx,0A
M01_L00:
mov r8d,ecx
sar r8d,1F
or r8d,1
sub eax,r8d
dec ecx
dec edx
jne short 00000000000023E7
ret
; Total bytes of code 28
In my benchmark, this alternative was a few nanoseconds faster for this example. It does not use cmovl, and the result does not establish that it will be faster in other cases; I would still be interested in whether C# can express a pattern that reliably leads the JIT to generate a conditional move.
Conclusion
This post examined how the .NET 11 JIT can generate a conditional move for a simple if-else, while emitting branches for similar logic inside a loop. A bit shift based alternative was a few nanoseconds faster in this benchmark, but it is not equivalent for every integer value and the result may not generalize to other workloads.