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 const or volatile).
  • 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, or unsigned 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:

  1. Eliminating Redundant Loads: Since the write to *b cannot logically alter the value stored at *a, the compiler does not issue an instruction to reload *a from memory. It simply returns the constant 5 directly from a register.
  2. 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.
  3. 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*, and signed char* are universally permitted to alias any other type. This allows standard memory operations (like memcpy and 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.