Type alignments and layouts in Delphi

2

Every Delphi type, built-in or user-defined, has 2 properties that affect memory placement of its instances: alignment and size. The alignment affects the starting address of an instance; for example the alignment of LongInt type is equal to 4, means that the starting address of any LongInt variable is a multiple of 4. The size is equal to the number of bytes occupied by an instance.

For any type, built-in or user-defined, the size is multiple of the alignment. The rule has only one notable exception, the FPU Extended type (and possibly user-defined types that include it) which has the alignment of 8 and the size of 10.

We can affect the alignment of user-defined types with {$ALIGN ..} compiler directive; there are 2 kinds of user-defined types possibly of interest in context of alignment – static arrays and records.

I will not tell here about the bugs in the alignment implementation in old Delphi versions; I believe they are fixed now and everything works as follows.

Static arrays are just not affected by the alignment directive. The alignment of a static array is equal to the alignment of an array element, the size of a static array is equal to the size of an array element multiplied by the number of elements in the array. Period.

As for records, there is common misunderstanding that the alignment directive defines the internal alignment of a record fields, i.e. the layout of a record type; it actually defines record type alignment, i.e. base address of a record instance in memory. But through defining the alignment it also indirectly defines the layout of a record type.

Here is how it works:

program AlignDemo;

{$APPTYPE CONSOLE}

{$ALIGN 1}
type
  TType1 = record
    Field1: Byte;
    Field2: int64;
  end;

{$ALIGN ON}
type
  TType2 = record
    Field1: Byte;
    Field2: int64;
  end;

begin
  Writeln('SizeOf(TType1) : ', SizeOf(TType1));    // 9
  Writeln('SizeOf(TType2) : ', SizeOf(TType2));    // 16
  Readln;
end.

With the TType1 we are telling the compiler that instances of TType1 are not aligned, i.e. can have any base address in memory; so it is impossible to make the TType1.Field2 8-byte aligned in memory by adding pad bytes between the Field1 and Field2.

With the TType2 we are telling the compiler to choose the optimal alignment; the compiler sets the alignment for TType2 equal to 8 because 8 is greatest alignment among the record fields’ types (it is the int64 type alignment). Since TType2 is 8-byte aligned, the compiler adds 7 padding bytes after TType2.Field1 to make the TType2.Field2 8-byte aligned too for performance reasons.

The case of more complicated structures is not much different – the compiler sets the record’s type alignment equal to the greatest alignment among the record fields if it is less than the default alignment; otherwise it uses the default alignment. This also unambiguously defines the internal layout because all alignments are powers of 2.

The story would be incomplete if I say nothing about the packed specifier. As the name suggests the packed specifier affects the layout of a record type. But following the same alignment-layout interdependence logic it also affects the record’s alignment, and the final result of applying the packed specifier is exactly the same as of {$ALIGN 1} directive. At least that is true for the current Delphi versions; there is the open bug report which suggests that it may change in future.

Advertisements