An Array Builder for Delphi

6 minute read

The design of an array builder in Delphi that holds records. Its purpose is to simplify the growth of an array as new elements are added, while improving speed by over-allocation

Motivation

One of the common requirements while working with a dynamic array is the need to resize the array as new elements are added. We often have certain conditions that are checked before an element is added, and the initial size of the array may not be known.

Delphi does have classes that are wrappers around a dynamic array. The simple lists TTList and TList<T> are examples of these, but these are heap managed types. Dynamic arrays are superior to lists as a result from functions. If we return lists there is always the question of ownership and who is responsible for memory management. Even worse, caller does not even need to receive the return value.

A Simple Example

We may have a trivial implementation such as this:

function PrimesBelow(MaxValue: integer): TArray<Integer>;
var
  Prime: integer;
begin
  Prime := 2;
  while Prime < MaxValue do
  begin
    SetLength(result, Length(Result) + 1);
    Result[High(result)] := Prime;
    Prime := NextPrime(Prime); // implementation not shown
  end;   
 end;

Not only is the code a bit ugly, it is quite inefficient to add one element at a time because dynamic array resizing involves copying the entire array.

Common attempts to improve performance

Resize in Blocks

The first and most common attempt to improve is to allocate in chunks rather than one at time:

function PrimesBelow(MaxValue: integer): TArray<Integer>;
var
  Prime: integer;
  Count: integer;
const
  BlockSize: 100;
begin
  Count := 0;  
  Prime := 2;
  while Prime < MaxValue do
  begin
    if Count > High(Result) then
      SetLength(result, Length(Result) + BlockSize);
    
    Result[Count] := Prime;
    Inc(Count);
    Prime := NextPrime(Prime); // implementation not shown
  end;   

   SetLength(Result, Count); // we have to trim overallocation at the end
 end;

We also have to keep track of the actual account and resize the final result as well. We have made the code even uglier, plus we allocate in chunks of a fixed size. We may want to be a bit smarter in our sizing.

Smart Initial Allocation

In the case of prime numbers there are functions to estimate the number of values that can be used in this case

function PrimesBelow(MaxValue: integer): TArray<Integer>;
var
  Prime: integer;
  Count: integer;
const
  BlockSize: 100;
begin
  Count := 0;  
  Prime := 2;

  SetLength(result, EstimatedNumberOfPrimesBelow(MaxValue)); // implementation not shown

  while Prime < MaxValue do
  begin
    // we might not be able to rely on our estimation
    // code and may need to check bounds anyway
    if Count > High(Result) then
      SetLength(result, Length(Result) + BlockSize);
    
    Result[Count] := Prime;
    Inc(Count);
    Prime := NextPrime(Prime); // implementation not shown
  end;   

  SetLength(Result, Count); 
end;

This code may be more performant, because of reducing the number of resizings of the array. However, the cost of the estimation function needs to be taken into account. Unfortunately, the code is still not very clean, in fact, uncertainty may lead us to still keep our bounds check and padding in place. We also probably would never have an exact size pre-calculated and we still need to fix the count at the end.

Smart Array Growth

The other common practice is to grow the dynamic arrays using a function of the current size. In the case of TList we resize by 1.5 times the previous size (some small values have their own steps). We could define a growth function such as this

procedure GrowArray(var Array: TArray<Double>); 
begin
  if Array = nil then // an dynamic array in Delphi that has no elements is nil
    SetLength(Array, 4)
  else if Length(Array) < 5 then
    SetLength(Array, 8)
  else if Length(Array) < 9 then
    SetLength(Array, 16)  
  else
    SetLength(Array, (Length(Array) * 3) div 2;
end;

This function can be called instead of using the SetLength(result, Length(Result) + BlockSize) call we did earlier. For instance if our prime count estimation was too expensive vs. resizing arrays we could change the code as follows

function PrimesBelow(MaxValue: integer): TArray<Integer>;
var
  Prime: integer;
  Count: integer;

begin
  Count := 0;  
  Prime := 2;
  while Prime < MaxValue do
  begin
    if Count > High(Result) then
      GrowArray(result); 
    
    Result[Count] := Prime;
    Inc(Count);
    Prime := NextPrime(Prime); // implementation not shown
  end;   

  SetLength(Result, Count);
 end;

If we create an implementation we may want to pass the growth function in so that we can grow according to a strategic pattern. I briefly touched on this in my previous post on record pools. We could define a more flexible function such as this

function GrowthFunction(Size: integer): integer; 
begin
  if size = 0 then
    exit(4);
    
  if size < 5 then
    exit(8);

  if size < 9 then
    exit(16);

  result := (Size * 3) div 2)
end;

This function can then be passed grow the array as needed. I’ll discuss this a bit later.

TArrayBuilder interface

The pattern above works quite well, but we can package all of this actions into a record that does all this for us.

unit ArrayBuilder;

interface

Type

  TArrayBuilder<T> = Record
  private
    FData: TArray<T>;
    FCount: Integer;

    procedure Grow;
  public
    class function Init(Size: Integer): TArrayBuilder<T>; static;

    // Record Initializers added in Delphi 10.4
    class operator Initialize(out Dest: TArrayBuilder<T>);

    procedure Add(const Element: T);
    function GetArray: TArray<T>;
  end;

implementation

class operator TArrayBuilder<T>.Initialize(out Dest: TArrayBuilder<T>);
const
  DefaultRec: TArrayBuilder<T> = (); // count = 0, dynamic array empty
begin
  Dest := DefaultRec;
end;

class function TArrayBuilder<T>.Init(Size: Integer): TArrayBuilder<T>;
begin
  SetLength(Result.FData, Size);
end;

procedure TArrayBuilder<T>.Add(const Element: T);
begin
  if FCount > High(FData) then
    Grow;

  FData[FCount] := Element;
  Inc(FCount);
end;

procedure TArrayBuilder<T>.Grow;
var
  NewSize: Integer;
begin

  if FData = nil then
    NewSize := 4
  else if Length(FData) < 5 then
    NewSize := 8
  else if Length(FData) < 9 then
    NewSize := 16
  else
    NewSize := (Length(FData) * 3) div 2;

  SetLength(FData, NewSize);
end;

function TArrayBuilder<T>.GetArray: TArray<T>;
begin
  SetLength(FData, FCount);
  Result := FData;
end;

end.

Our code can now look like this:

function PrimesBelow(MaxValue: integer): TArray<Integer>;
var
  Prime: integer;
  PrimeBuilder: TArrayBuilder<Integer>;
begin
  Prime := 2;
  while Prime < MaxValue do
  begin
    PrimeBuilder.Add(Prime)  
    Prime := NextPrime(Prime); // implementation not shown
  end;   

  result := PrimeBuilder.GetArray;
end;

If we had a fast estimator for the number of primes we could initialize with that to improve performance, while keeping the code still relatively clean

var
  Prime: integer;  
  PrimeBuilder: TArrayBuilder<Integer>;
begin
  PrimeBuilder := TArrayBuilder<Integer>.Init(
     EstimatedNumberOfPrimesBelow(MaxValue)); 
  Prime := 2;
  //... 

Custom Growth Function

Our Array Builder can now be extended by allowing a growth function that can either be held by the TArrayBuilder<T> or passed into an overload of the Add(Element: T) method.

Here is an implementation using the function as a member of the TArrayBuilder<T>:

unit ArrayBuilder;

interface

Type

  TGrowthFunction = reference to function(CurrentSize: Integer): Integer;

  TArrayBuilder<T> = Record
  private
    FData: TArray<T>;
    FCount: Integer;
    FGrowthFunction: TGrowthFunction;
  public
    class function Init(Size: Integer): TArrayBuilder<T>; static;

    // Record Initializers added in Delphi 10.4
    class operator Initialize(out Dest: TArrayBuilder<T>);

    procedure Add(const Element: T);
    function GetArray: TArray<T>;
    procedure SetGrowthFunction(const F: TGrowthFunction);
  end;

  // due to the way Generics resolve I have to make the default growth function public
  function DefaultArrayBuilderGrowthFunction(Size: integer): integer;

implementation

function DefaultArrayBuilderGrowthFunction(Size: integer): integer;
begin
  if size = 0 then
    exit(4);

  if size < 5 then
    exit(8);

  if size < 9 then
    exit(16);

  result := (Size * 3) div 2;
end;


class operator TArrayBuilder<T>.Initialize(out Dest: TArrayBuilder<T>);
const
  DefaultRec: TArrayBuilder<T> = (); // count = 0, dynamic array empty
begin
  Dest := DefaultRec;
  Dest.FGrowthFunction := DefaultArrayBuilderGrowthFunction;
end;

procedure TArrayBuilder<T>.SetGrowthFunction(const F: TGrowthFunction);
begin
  FGrowthFunction := F;
end;

class function TArrayBuilder<T>.Init(Size: Integer): TArrayBuilder<T>;
begin
  SetLength(Result.FData, Size);
end;

procedure TArrayBuilder<T>.Add(const Element: T);
begin
  if FCount > High(FData) then
     SetLength(FData, FGrowthFunction(Length(FData)));

  FData[FCount] := Element;
  Inc(FCount);
end;

function TArrayBuilder<T>.GetArray: TArray<T>;
begin
  SetLength(FData, FCount);
  Result := FData;
end;

end.

We can now provide a custom growth function in this form

SomeArrayBuilder.SetGrowthFunction(
  function (Size: integer): integer 
  begin
    result := Size + 100;
  end
);

Conclusion

I hope that you found this useful or at least interesting. Please download, clone or fork the source code.

Comments and feedback are always welcomed

Leave a Comment