You may be a compiler writer if ...

Some time ago, in a (probably misguided) attempt to adapt at runtime to a bit of endian weirdness, I wrote some code that looked more or less like

   int64_t buffer[2];
   volatile int64_t *a = (volatile int64_t*)buffer;
   volatile int32_t *b = (volatile int32_t*)buffer;
   *a = 1;
   if( *b == 1 ) doSomeStuff();
There was a bit more to it, but this is representative. This code worked well for quite some time, until we needed to move the program that it is part of to machine with a different processor architecture. When we turned on optimization in the compiler, the program stopped working.

Frantic debugging ensued. Eventually I discovered that the optimizing compiler turns the snippet above into something like this (retouched into hypothetical pseudo assembly such as to protect the innocent):

   add    sp,#24,r13
   mov    #0,r3
   load   0(r13),r5
   mov    #1,r4
   store  r3,4(r13)
   cmp    #1,r5
   store  r4,0(r13)
   bsr.eq doSomeStuff

Now, an ordinary competent programmer should be able to understand that C99's strict aliasing rules authorize a compiler to assume that *a and *b refer to different objects, and therefore the optimizer may reorder the code such that the read of *b happens before *a is written. I admit it surprised me a bit that the volatile qualifiers did not prevent this, which seems to violate the letter of the C standard regarding volatile accesses and sequence points. However, apparently the compiler in question does not recognize any ordering constraints on accesses to "different" volatile-qualified objects within a basic block.

However, notice that even though the compiler is not smart enough to know that the two pointers alias, it is smart enough to figure out that they can share the same register! If you think that is perfectly reasonable and understandable, you're seriously at risk of being a compiler writer at heart.

For the record, I do and I am.

P.S. The Right way to code this is type-pun through an union instead:

   union {
     int64_t a[2];
     int32_t b;
   } buffer;
   buffer.a[0] = 1;
   if( buffer.b == 1 ) doSomeStuff();

which in this case compiled into an uncoditional call to doSomeStuff. Understanding Strict Aliasing by Mike Acton explains both the rules and the cheats that nevertheless work with admirable clarity. Respectfully recommended for your edification.

No comments:

Post a Comment