What is the rationale behind the strict aliasing rule?

Viewed 244

I am currently wondering about the rationale behind the strict aliasing rule. I understand that certain aliasing is not allowed in C and that the intention is to allow optimizations, but I am surprised that this was the preferred solution over tracing type casts when the standard was defined.

So, apparently the following example violates the strict aliasing rule:

uint64_t swap(uint64_t val)
{
    uint64_t copy = val;
    uint32_t *ptr = (uint32_t*)© // strict aliasing violation
    uint32_t tmp = ptr[0];
    ptr[0] = ptr[1];
    ptr[1] = tmp;
    return copy;
}

I might be wrong, but as far as I can see a compiler should perfectly and trivially be able to trace down the type casts and avoid optimizations on types which are casted explicitly (just like it avoids such optimizations on same-type pointers) on anything called with the affected values.

So, which problems with the strict aliasing rule did I miss that a compiler can't solve easily to automatically detect possible optimizations)?

4 Answers

Since, in this example, all the code is visible to a compiler, a compiler can, hypothetically, determine what is requested and generate the desired assembly code. However, demonstration of one situation in which a strict aliasing rule is not theoretically needed does nothing to prove there are not other situations where it is needed.

Consider if the code instead contains:

foo(&val, ptr)

where the declaration of foo is void foo(uint64_t *a, uint32_t *b);. Then, inside foo, which may be in another translation unit, the compiler would have no way of knowing that a and b point to (parts of) the same object.

Then there are two choices: One, the language may permit aliasing, in which case the compiler, while translating foo, cannot make optimizations relying on the fact that *a and *b are different. For example, whenever something is written to *b, the compiler must generate assembly code to reload *a, since it may have changed. Optimizations such as keeping a copy of *a in registers while working with it would not be allowed.

The second choice, two, is to prohibit aliasing (specifically, not to define the behavior if a program does it). In this case, the compiler can make optimizations relying on the fact that *a and *b are different.

The C committee chose option two because it offers better performance while not unduly restricting programmers.

It allows the compiler to optimize out variable reloads without requiring that you restrict-qualify your pointers.

Example:

int f(long *L, short *S)
{
    *L=42;
    *S=43;
    return *L;
}

int g(long *restrict L, short *restrict S)
{
    *L=42;
    *S=43;
    return *L;
}

Compiled with strict aliasing disabled (gcc -O3 -fno-strict-aliasing) on x86_64 :

f:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movq    (%rdi), %rax ; <<*L reloaded here cuz *S =43 might have changed it
        ret
g:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax     ; <<42 constant-propagated from *L=42 because *S=43 cannot have changed it  (because of `restrict`)
        ret

Compiled with gcc -O3 (implies -fstrict-alising) on x86_64:

f:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax   ; <<same as w/ restrict
        ret
g:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax
        ret

https://gcc.godbolt.org/z/rQDNGt

This can help quite a bit when you're working with large arrays which might otherwise lead to a lot of unnecessary reloads.

Programming languages are specified to support what the members of standardisation committee believe is reasonable, common sense practice. The use of different pointers of very significantly different types aliasing the same object was believe to be unreasonable and something that compilers should not have to go out of the way to make possible.

Such code:

float f(int *pi, float *pf) {
  *pi = 1;
  return *pf;
}

when used with both pi and pf holding the same address, where *pf is meant to reinterpret the bits of the recently written *pi, is considered unreasonable thus the honorable committee members (and before them the designers of the C language) did not believe it was appropriate to require compiler to avoid common sense program transformation in a slightly more complicated example:

float f(int *pi, double *pf) {
  (*pi)++;
  (*pf) *= 2.;
  (*pi)++;
}

Here allowing the corner case where both pointers point to the same object would make any simplification where the increments are fused invalid; assuming such aliasing does not occur allows the code to be compiled as:

float f(int *pi, double *pf) {
  (*pf) *= 2.;
  (*pi) += 2;
}

The footnote to N1570 p6.5p7 clearly states the purpose of the rule: to say when things may alias. As to why the rule is written so as to forbid constructs like yours which don't involve aliasing as written (since all accesses using the uint32_t* are performed in contexts where it is visibly freshly derived from the uint64_t, that's most likely because the authors of the Standard recognized that anyone making a bona fide effort to produce a quality implementation suitable for low-level programming would support constructs like yours (as a "popular extension") whether or not the Standard mandated it. This same principle appears more explicitly with regard to constructs like:

unsigned mulMod65536(unsigned short x, unsigned short y)
{ return (x*y) & 65535u; }

According to the Rationale, commonplace implementations will process operations on short unsigned values in a fashion equivalent to unsigned arithmetic even if the result is between INT_MAX+1u and UINT_MAX, except when certain conditions apply. There's no need to have a special rule to make the compiler treat expressions involving short unsigned types as unsigned when the results are coerced to unsigned because--according to the authors of the Standard--commonplace implementations do that even without such a rule.

The Standard was never intended to fully specify everything that should be expected of a quality implementation that claims to be suitable for any particular purpose. Indeed, it doesn't even require that implementations be suitable for any useful purpose whatsoever (the Rationale even acknowledges the possibility of a "conforming" implementation that is of such poor quality that cannot meaningfully process anything other than a single contrived and useless program).

Related