Memory Ordering and Other Porting Issues#

This chapter describes the memory ordering and volatile semantics that change when code moves from x86/x86_64 hardware to ARM64. The following sections cover those semantics, a recommended porting strategy, the architectural differences that can prevent ported code from working, and the compiler intrinsic to use for memory barriers.

Memory Ordering and Volatile Semantics#

x86/x86_64 enforces a Total Store Order (TSO)-style memory ordering model in hardware. This allows developers to omit volatile on shared variables or pointers without consequence. ARM64 requires explicit memory ordering because the ARM architecture does not make that promise at the hardware level.

The volatile keyword results in the compiler’s default /volatile:iso option, which does not guarantee the acquire or release semantics required for total store ordering. Using /volatile:ms on ARM64 guarantees acquire or release semantics for volatile access. ARM64EC enforces x86/x86_64-style ordering by default by generating code that ensures total store ordering.

Hardware Differences Between x86/x86_64 and ARM#

The following architectural differences can cause code ported to ARM64 not to work as expected.

Memory ordering. See Memory Ordering and Volatile Semantics and Memory Barriers with a Compiler Intrinsic.

Unaligned memory access. On x86/x86_64, unaligned accesses have always been legal. ARM64 CPU allows unaligned regular loads and stores to Normal (cached) memory when the OS does not enable strict alignment checking. Windows leaves that check off, so the CPU handles those accesses in hardware and the kernel does not trap-and-emulate them for native ARM64 binaries. Unaligned accesses can still be slower than aligned ones and do not provide single-copy atomicity, so they can race and should be avoided. Code that uses pointer arithmetic can run into this issue (however, normal C variables and structs are properly aligned). Accesses to Device (uncached) memory must still be aligned. Unaligned atomics are a separate case.

NaN representation. When a floating-point square root of a negative number, or another operation, produces an indeterminate NaN result, x86/x86_64 represents that result differently from ARM by design. This is normally not an issue, except when code casts a float to an int and checks for specific bit patterns, which can fail when ported.

Unaligned atomics. Unlike x86/x86_64, all ARM cores raise an exception when unaligned atomics cross a 16-byte boundary. An unaligned atomic crashes a native ARM64 binary; there is no trap-and-emulate for native code. Under emulation, the unaligned address is detected and the emulator pauses all threads in the process to serialize that memory access. This is more than 1000x slower than a plain native aligned atomic access, so unaligned atomics can severely degrade performance under emulation. Unaligned interlocked access on ARM64 is not atomic-safe.

Self-modifying code and cache coherency. Self-modifying code is less constrained on x86/x86_64 processors than on ARM. The x86/x86_64 manuals specify that, after modifying code, a branch such as JMP must execute to force a refetch of the instruction-fetch queue. From the Windows perspective, an application that dynamically generates or backpatches code must call FlushInstructionCache() to inform the operating system. The emulator, along with callbacks from the Windows memory manager, detects self-modifying code and flushes both hardware caches and translation caches as appropriate.

Data structure alignment rules. ARM64 has stricter stack-alignment rules that hardware enforces: the stack pointer must be 16-byte aligned at all times or an exception is raised. 32-bit x86 does not enforce stack alignment in hardware; subtracting an odd value from the stack pointer does not raise an exception. x86_64 uses 16-byte alignment as a software convention, but this is not hardware-enforced. This can be problematic when code assumes local-variable stack alignment or uses pointer arithmetic to access function arguments. Emulation works around this by maintaining two stack pointers. The native ARM64 stack-pointer register is the emulator’s private hardware stack, while the emulated x86/x86_64 stack pointer is stored in X28. Because X28 is a general-purpose register, it has no hardware alignment requirements and can emulate arbitrary x86/x86_64 stack alignment.

Memory Barriers with a Compiler Intrinsic#

The compiler intrinsic _ReadWriteBarrier() is deprecated, does nothing on ARM, and should be replaced with MemoryBarrier().

Additional Resources#

For more information, see ARM64EC Explained and ARM64 Boot Camp: The Windows on ARM Tutorials.