Understanding the Strict Aliasing Rule in C
The strict aliasing rule is a core semantic constraint in the C programming language that dictates how different pointer types access the same memory location. By establishing that two pointers of incompatible types cannot point to (or "alias") the same physical address, the rule allows modern compilers to employ Type-Based Alias Analysis (TBAA). This allows the compiler to make safe assumptions about memory independence, eliminating redundant read and write operations, reordering instructions, and keeping frequently accessed values cached inside CPU registers for significantly improved runtime performance.
The Concept of Pointer Aliasing
In C, aliasing occurs when two or more pointers refer to the exact same block of memory. When a value is modified through one pointer, any subsequent read through an aliased pointer must reflect that change.
Consider this simple function:
void compute(int *a, double *b) {
*a = 5;
*b = 3.14;
return *a;
}If the compiler had to consider the possibility that a
and b pointed to the same byte address in RAM, it would be
forced to write 5 to *a, write
3.14 to *b, and then reload the value of
*a from memory to see if *b inadvertently
modified it. This repeated memory access severely degrades execution
speed.
How the Strict Aliasing Rule Works
The C standard (specifically section 6.5, paragraph 7 of C99 and later) defines the strict aliasing rule to eliminate this performance bottleneck. It explicitly declares that an object's stored value may only be accessed by an lvalue expression of:
- An identical or compatible type.
- A qualified version of the type (such as
constorvolatile). - A type that is the signed or unsigned equivalent of the type.
- An aggregate or union type that includes one of the aforementioned types among its members.
- A character type (
char,signed char, orunsigned char).
Because int and double are incompatible
types and neither is a character type, the compiler relies on the C
standard to assume that a and b can never
overlap.
How Compilers Exploit the Rule
When strict aliasing is assumed—often managed via compiler flags like
-O2 or -fstrict-aliasing in GCC and Clang—the
optimizer runs Type-Based Alias Analysis (TBAA). The compiler
categorizes memory references into distinct type sets:
- Eliminating Redundant Loads: Since the write to
*bcannot logically alter the value stored at*a, the compiler does not issue an instruction to reload*afrom memory. It simply returns the constant5directly from a register. - Instruction Scheduling and Reordering: The compiler is free to reorder the instructions—executing the floating-point store before or alongside the integer store—to better saturate hardware pipelines and minimize CPU pipeline stalls.
- Dead Store Elimination: If a variable is updated, but an incompatible pointer writes to an arbitrary address afterward, the compiler knows the original variable was not touched and can eliminate dead or intermediate stores.
Exceptions to the Rule
The standard provides designated exceptions so developers can legitimately inspect raw memory:
- Character Pointers: Pointers of type
char*,unsigned char*, andsigned char*are universally permitted to alias any other type. This allows standard memory operations (likememcpyand byte-by-byte inspection) to function correctly without breaking compiler assumptions. - Unions (Type Punning): While casting a pointer
directly (e.g.,
*(float*)&my_int) invokes undefined behavior under strict aliasing, reading from a different union member than the one last written to is explicitly permitted in modern C (C99 and later) as a valid method for type punning.
Undefined Behavior and Miscompilation
Violating the strict aliasing rule by casting incompatible pointer
types produces undefined behavior. Because the compiler assumes these
types never overlap, it may reorder memory operations such that a read
occurs before a write, resulting in stale data or memory corruption that
often only appears when optimization flags are enabled. Safe cross-type
conversions should always be handled using character copies, standard
union constructs, or the memcpy() function, which compilers
routinely optimize into native register loads.