Why .NET 10 JIT Physical Promotion of Structs Fails on Windows x64
Introduction
With the release of .NET 10, developers are excited to test new performance features like physical promotion (also referred to as "improved code generation for struct arguments"). This optimization allows the JIT compiler to pass struct fields directly in registers instead of allocating the struct on the stack. However, you might find that reproducing this optimization on your local machine doesn't work as expected.
If you've run the sample code on Windows and noticed the JIT is still passing your struct by reference, you are not alone. Here is why this happens and how to fix it.
The Root Cause: Windows x64 ABI vs. SysV (Linux/macOS) ABI
The discrepancy between the official .NET 10 documentation and your local test results comes down to the Application Binary Interface (ABI) of your operating system. The .NET JIT must conform to the platform's native calling convention to ensure compatibility with native code and system APIs.
1. Windows x64 Calling Convention
On Windows x64, the calling convention dictates that any struct larger than 8 bytes (64 bits) cannot be passed directly in registers. Instead, the caller must allocate the struct on the stack and pass a pointer (reference) to it in a register (typically rcx). Since your Point struct contains two long fields (totaling 16 bytes), the Windows ABI strictly requires it to be passed by reference.
2. SysV AMD64 Calling Convention (Linux/macOS)
In contrast, the SysV ABI used by Linux and macOS is much more flexible. It allows structs up to 16 bytes to be classified and passed directly in up to two registers (such as rdi and rsi).
If you look closely at the expected assembly from the .NET 10 documentation:
mov edi, 10
mov esi, 20
tail.jmp [Program:Consume(Program+Point)]The registers edi and esi are the standard first and second argument registers for the SysV ABI. This confirms that the documentation example was run and captured on a Linux or macOS environment, not Windows.
How to Verify and Work Around This Limit
If you want to see .NET 10's physical promotion in action, you have a few options depending on your target environment:
Option 1: Run on Linux (WSL) or macOS
The easiest way to replicate the documentation's output is to run your code on a non-Windows OS. If you are on Windows 11, you can easily use Windows Subsystem for Linux (WSL):
- Install the .NET 10 SDK inside your WSL distribution.
- Run the same
dotnet runcommand with JIT disassembly enabled. - You will see the struct fields successfully promoted to registers!
Option 2: Reduce the Struct Size to 8 Bytes (for Windows)
If you must run on Windows, you can observe register passing by shrinking the struct so it fits within the Windows x64 ABI limit of 8 bytes. For example, change the long fields to int:
struct Point
{
public int X;
public int Y;
public Point(int x, int y) { X = x; Y = y; }
}With a total size of 8 bytes, the JIT can pack both 32-bit integers into a single 64-bit register (rcx) on Windows, avoiding stack allocation.
Option 3: Allow Inlining
The ABI restrictions only apply to physical method boundaries (where a formal call instruction is made). If you remove the [MethodImpl(MethodImplOptions.NoInlining)] attribute, the JIT will likely inline the Consume method. Once inlined, the ABI boundary disappears, and the JIT's physical promotion engine can optimize the struct fields directly into registers locally, even on Windows.
Conclusion
The lack of register passing for 16-byte structs on Windows is not a bug or a failure of the .NET 10 JIT. It is a strict adherence to the Windows x64 calling convention. By understanding how platform ABIs affect code generation, you can write highly optimized, cross-platform .NET applications that leverage physical promotion where the OS allows it.