6.3.1.9 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;
   ...

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

Here, 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) for the GCC back end and -O1 or higher for the LLVM back end 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, you can run into difficulties arise if you use features like Unchecked_Conversion 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);
   begin
      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);
begin
   V1.all := 1;
   V2.all := 0;
   Put_Line (Int1'Image (V1.all));
end;

This program prints out 0 in -O0 mode, 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, you probably expect 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 higher levels of optimization, but that is quite drastic, since it throws away a number of useful optimizations that don’t involve strict aliasing assumptions.

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

To avoid the use of compiler switches, you may use the configuration pragma No_Strict_Aliasing with no parameters 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, you 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’s 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 for you to use pragma No_Strict_Aliasing for the type. You must place this pragma 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’s very powerful and effectively takes Int2 out of the alias analysis performed by the compiler in all circumstances.

You can also use this pragma used to deal with aliasing issues that arise 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);
begin
   Send (To_Stream (R));
end;

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’s 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, you can 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);

You can also apply this pragma 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 summarize, the alias analysis performed in strict aliasing mode by the compiler can have significant benefits. We’ve 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 (with the GCC back end) 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.