Question

I have an advanced record with a dynamic array field. The record has a class operator for concatenation of a record and a byte. Also an Add method, adding a byte.

For what I'm about to use the record, the reference count of the dynamic array field is of importance. When running the two test procedures below, you can see that the concatenation results in a reference count of 2 while the add method results in a reference count of 1.

program TestReferenceCount;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

Type
  TRec = record
    class operator Add(const a: TRec; b: Byte): TRec;
  private type
    PDynArrayRec = ^TDynArrayRec;
    TDynArrayRec = packed record
      {$IFDEF CPUX64}
      _Padding: LongInt; // Make 16 byte align for payload..
      {$ENDIF}
      RefCnt: LongInt;
      Length: NativeInt;
    end;
  private
    FArr: TBytes;
    function GetRefCnt: Integer;
  public
    procedure Add(b : Byte);
    property RefCnt: Integer read GetRefCnt;
  end;

procedure TRec.Add(b : Byte);
var
  prevLen: Integer;
begin
  prevLen := System.Length(Self.FArr);
  SetLength(Self.FArr, prevLen + 1);
  Self.FArr[prevLen] := b;
end;

class operator TRec.Add(const a: TRec; b: Byte): TRec;
var
  aLen: Integer;
begin
  aLen := System.Length(a.FArr);
  SetLength(Result.FArr, aLen + 1);
  System.Move(a.FArr[0], Result.FArr[0], aLen);
  Result.FArr[aLen] := b;
end;

function TRec.GetRefCnt: Integer;
begin
  if Assigned(FArr) then
    Result := PDynArrayRec(NativeInt(FArr) - SizeOf(TDynArrayRec)).RefCnt
  else
    Result := 0;
end;

procedure TestConcatenation;
var
  r1 : TRec;
begin
  WriteLn('RC:', r1.RefCnt);  // <-- Writes 0
  r1 := r1 + 65;
  WriteLn('RC:', r1.RefCnt);  // <-- Writes 2
end;

procedure TestAdd;
var
  r1 : TRec;
begin
  WriteLn('RC:', r1.RefCnt); // <-- Writes 0
  r1.Add(65);
  WriteLn('RC:', r1.RefCnt); // <-- Writes 1
end;

begin
  TestConcatenation;
  TestAdd;
  ReadLn;
end.

The compiler takes care of the extra reference count when the record variable goes out of scope, so no problem really at this point.

But can this behavior be explained? Is it an undocumented implementation detail? Is there a way to avoid the extra count?

Was it helpful?

Solution

Let's take a look at this function:

procedure TestConcatenation;
var
  r1 : TRec;
begin
  r1 := r1 + 65;
end; 

The compiler actually implements it like this:

procedure TestConcatenation;
var
  r1 : TRec;
  tmp : TRec;
begin
  tmp := r1 + 65;
  r1 := tmp;
end; 

The compiler introduces a temporary local to store the result of r1 + 65. There's a very good reason for that. If it did not, where would it write the result of your addition operator? Since the ultimate destination is r1, if your addition operator writes directly to r1 it is modifying its input variable.

There is no way to stop the compiler generating this temporary local.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top