Optimization and Strict Aliasing

The strong typing capabilities of Ada allow an optimizer to generate efficient code in situations where other languages would be forced to make worst case assumptions preventing such optimizations. Consider the following example:

procedure M is
   type Int1 is new Integer;
   I1 : Int1;

   type Int2 is new Integer;
   type A2 is access Int2;
   V2 : A2;

   for J in Data'Range loop
      if Data (J) = I1 then
         V2.all := V2.all + 1;
      end if;
   end loop;

In this example, since V2 can only access objects of type Int2 and I1 is not one of them, there is no possibility that the assignment to V2.all affects the value of I1. This means that the compiler optimizer can infer that the value I1 is constant for all iterations of the loop and load it from memory only once, before entering the loop, instead of in every iteration (this is called load hoisting).

This kind of optimizations, based on strict type-based aliasing, is triggered by specifying an optimization level of -O2 or higher (or -Os) and allows the compiler to generate more efficient code.

However, although this optimization is always correct in terms of the formal semantics of the Ada Reference Manual, difficulties can arise if features like Unchecked_Conversion are used to break the typing system. Consider the following complete program example:

package P1 is
   type Int1 is new Integer;
   type A1 is access Int1;

   type Int2 is new Integer;
   type A2 is access Int2;
end P1;

with P1; use P1;
package P2 is
   function To_A2 (Input : A1) return A2;
end p2;

with Ada.Unchecked_Conversion;
package body P2 is
   function To_A2 (Input : A1) return A2 is
      function Conv is
        new Ada.Unchecked_Conversion (A1, A2);
      return Conv (Input);
   end To_A2;
end P2;

with P1; use P1;
with P2; use P2;
with Text_IO; use Text_IO;
procedure M is
   V1 : A1 := new Int1;
   V2 : A2 := To_A2 (V1);
   V1.all := 1;
   V2.all := 0;
   Put_Line (Int1'Image (V1.all));

This program prints out 0 in -O0 or -O1 modes, but it prints out 1 in -O2 mode. That’s because in strict aliasing mode, the compiler may and does assume that the assignment to V2.all could not affect the value of V1.all, since different types are involved.

This behavior is not a case of non-conformance with the standard, since the Ada RM specifies that an unchecked conversion where the resulting bit pattern is not a correct value of the target type can result in an abnormal value and attempting to reference an abnormal value makes the execution of a program erroneous. That’s the case here since the result does not point to an object of type Int2. This means that the effect is entirely unpredictable.

However, although that explanation may satisfy a language lawyer, in practice an application programmer expects an unchecked conversion involving pointers to create true aliases and the behavior of printing 1 is questionable. In this case, the strict type-based aliasing optimizations are clearly unwelcome.

Indeed the compiler recognizes this possibility and the instantiation of Unchecked_Conversion generates a warning:

p2.adb:5:07: warning: possible aliasing problem with type "A2"
p2.adb:5:07: warning: use -fno-strict-aliasing switch for references
p2.adb:5:07: warning:  or use "pragma No_Strict_Aliasing (A2);"

Unfortunately the problem is only recognized when compiling the body of package P2, but the actual problematic code is generated while compiling the body of M and this latter compilation does not see the suspicious instance of Unchecked_Conversion.

As implied by the warning message, there are approaches you can use to avoid the unwanted strict aliasing optimizations in a case like this.

One possibility is to simply avoid the use of -O2, but that is quite drastic, since it throws away a number of useful optimizations that do not involve strict aliasing assumptions.

A less drastic approach is to compile the program using the option -fno-strict-aliasing. Actually it is only the unit containing the dereferencing of the suspicious pointer that needs to be compiled. So in this case, if we compile unit M with this switch, then we get the expected value of zero printed. Analyzing which units might need the switch can be painful, so a more reasonable approach is to compile the entire program with options -O2 and -fno-strict-aliasing. If the performance is satisfactory with this combination of options, then the advantage is that the entire issue of possible problematic optimizations due to strict aliasing is avoided.

To avoid the use of compiler switches, the configuration pragma No_Strict_Aliasing with no parameters may be used to specify that for all access types, the strict aliasing optimizations should be suppressed.

However, these approaches are still overkill, in that they cause all manipulations of all access values to be deoptimized. A more refined approach is to concentrate attention on the specific access type identified as problematic.

The first possibility is to move the instantiation of unchecked conversion to the unit in which the type is declared. In this example, we would move the instantiation of Unchecked_Conversion from the body of package P2 to the spec of package P1. Now the warning disappears because any use of the access type knows there is a suspicious unchecked conversion, and the strict aliasing optimizations are automatically suppressed for it.

If it is not practical to move the unchecked conversion to the same unit in which the destination access type is declared (perhaps because the source type is not visible in that unit), the second possibiliy is to use pragma No_Strict_Aliasing for the type. This pragma must occur in the same declarative part as the declaration of the access type:

type A2 is access Int2;
pragma No_Strict_Aliasing (A2);

Here again, the compiler now knows that strict aliasing optimizations should be suppressed for any dereference made through type A2 and the expected behavior is obtained.

The third possibility is to declare that one of the designated types involved, namely Int1 or Int2, is allowed to alias any other type in the universe, by using pragma Universal_Aliasing:

type Int2 is new Integer;
pragma Universal_Aliasing (Int2);

The effect is equivalent to applying pragma No_Strict_Aliasing to every access type designating Int2, in particular A2, and more generally to every reference made to an object of declared type Int2, so it is very powerful and effectively takes Int2 out of the alias analysis performed by the compiler in all circumstances.

This pragma can also be used to deal with aliasing issues that arise again from the use of Unchecked_Conversion in the source code but without the presence of access types. The typical example is code that streams data by means of arrays of storage units (bytes):

type Byte is mod 2**System.Storage_Unit;
for Byte'Size use System.Storage_Unit;

type Chunk_Of_Bytes is array (1 .. 64) of Byte;

procedure Send (S : Chunk_Of_Bytes);

type Rec is record
end record;

procedure Dump (R : Rec) is
   function To_Stream is
      new Ada.Unchecked_Conversion (Rec, Chunk_Of_Bytes);
   Send (To_Stream (R));

This generates the following warning for the call to Send:

dump.adb:8:25: warning: unchecked conversion implemented by copy
dump.adb:8:25: warning: use pragma Universal_Aliasing on either type
dump.adb:8:25: warning: to enable RM 13.9(12) implementation permission

This occurs because the formal parameter S of Send is passed by reference by the compiler and it is not possible to pass a reference to R directly in the call without violating strict type-based aliasing. That’s why the compiler generates a temporary of type Chunk_Of_Bytes just before the call and passes a reference to this temporary instead.

As implied by the warning message, it is possible to avoid the temporary (and the warning) by means of pragma Universal_Aliasing:

type Chunk_Of_Bytes is array (1 .. 64) of Byte;
pragma Universal_Aliasing (Chunk_Of_Bytes);

The pragma can also be applied to the component type instead:

type Byte is mod 2**System.Storage_Unit;
for Byte'Size use System.Storage_Unit;
pragma Universal_Aliasing (Byte);

and every array type whose component is Byte will inherit the pragma.

To sum up, the alias analysis performed in strict aliasing mode by the compiler can have significant benefits. We have seen cases of large scale application code where the execution time is increased by up to 5% when these optimizations are turned off. However, if you have code that make significant use of unchecked conversion, you might want to just stick with -O1 and avoid the entire issue. If you get adequate performance at this level of optimization, that’s probably the safest approach. If tests show that you really need higher levels of optimization, then you can experiment with -O2 and -O2 -fno-strict-aliasing to see how much effect this has on size and speed of the code. If you really need to use -O2 with strict aliasing in effect, then you should review any uses of unchecked conversion, particularly if you are getting the warnings described above.