2

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?

6
  • Speaking of undocumented implementation details, relying on the reference count of a dynamic array to have any particular value in the first place seems like a brittle design. Commented Jun 10, 2013 at 21:46
  • @RobKennedy, not sure what you mean, why would you not trust a reference count of a dynamic array? Like a string it will remove itself from the heap when the count goes to zero. Commented Jun 10, 2013 at 21:51
  • Of course, but who says when it will be zero? The documentation doesn't give an exhaustive list of when a reference count will change. It only says when the reference count will be 1, and what happens when it changes to zero. It never says when it changes to zero. Commented Jun 10, 2013 at 21:54
  • 1
    @DavidHeffernan, I have implemented a COW pattern. I have lots of ascii-based protocols and binary protocols using ansistrings. After Delphi-2007, I made a TByteString record which could mimic most of the ansistring functions, since the behavior of ansistrings was not predictable anymore. I was missing the COW though, and now it is working ok, no real performance penalty. I will make this public when ready. There will be space for a codepage, but I will not implement the unicode part, at least not for the moment. Commented Jun 10, 2013 at 22:57
  • 1
    By the way, I congratulate you for providing a compilable code sample. If only more questions did that! Commented Jun 11, 2013 at 7:44

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

..., if your adition operator writes directly to r1 it is modifying its input value. That's it! I've got it, thanks. I could see asm code, but could not understand the reason.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.