Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird class instance sizing using class members that are arrays of generic types

Below is a very simple bit of code that mimics a class structure in some code I have (the form just contains a single button attached to the click event). I am using Delphi XE and XE II and see nasty crashes when destroying objects in my production code that this class is based on. These crashes only occur if I allow the members of the array item in TMyClass to be initialised in the Clear() method.

Unfortunately the crash is not repeatable so far in this sample code, but it does mimic some very weird behaviour with instance sizing that I suspect might be the cause of the problem.

If I place a break point in the TMyClass.Clear function and look at the InstanceSize member of the class reports 1288. Which is odd, as the size of the array member alone is really 12kb. I can change the type being provided from TRecord2 to Integer and I get the same InstanceSize result. If I remove the array entirely from the class I get an Instance size of ~264 bytes, which seems over the top for a class that contains only a single 128 byte record instance. This indicates that the compiler has allocated 1024 bytes to the array storage for the V (of type TT) member in TMyClass.

I am suspicious that the weird instance size is causing bad things to happen when I write 12kb of data into an object that the compiler thinks is 1kb in size.

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

type
  TRecord = record A : Array[1..128] of byte; end;
  TRecord2 = packed record S : Single; T : TDateTime; end;

  TBase = class(TObject)
    public
      R : TRecord;
  end;

  TBase2<T> = class(TBase)
    public
      type
        TT = packed array [0..31, 0..31] of T;

      var
        V : TT;
        R2 : TRecord;
  end;

  TMyClass = class(TBase2<TRecord2>)
    public
      procedure Clear;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  O : TMyClass;
begin
  O := TMyClass.Create;
  O.Clear;
  O.Free;
end;

{ TMyClass }

procedure TMyClass.Clear;
var
  i, j : integer;
begin
  for i := 0 to 31 do
    for j := 0 to 31 do
      begin
        V[I, J].S := 0;
        V[I, J].T := 0;
      end;
end;

end.
like image 373
Raymond Wilson Avatar asked Oct 28 '11 18:10

Raymond Wilson


1 Answers

It's a bug, as is shown by this minimal reproduction:

program InstanceSizeBug;

{$APPTYPE CONSOLE}

type
  TMyClass<T> = class
    V: array [0..255] of T;
  end;
  TMyClassSingle = class(TMyClass<Single>);

begin
  Writeln(TMyClass<Single>.InstanceSize);
  Writeln(TMyClassSingle.InstanceSize);
  Readln;
end.

Output:

1032
264

Note that the same behaviour can be observed in Delphi 2010, the only other Delphi version i have to hand.

Trace through under the debugger and you find yourself in _GetMem with the Size parameter equal to 264 so clearly not enough memory is being allocated.

And just in case there is any doubt, this version fails with an AV.

program InstanceSizeBug;

{$APPTYPE CONSOLE}

type
  TMyClass<T> = class
    V: array [1..256*256] of T;
  end;
  TMyClassSingle = class(TMyClass<Single>);

begin
  TMyClassSingle.Create.V[256*256] := 0;
end.

I have submitted this to Quality Central, issue #100561.

As a workaround I suggest you use a dynamic array which you allocate using SetLength in the constructor. I rather imagine that it is the presence of a fixed sized generic array that is making the compiler misbehave.

like image 75
David Heffernan Avatar answered Oct 21 '22 10:10

David Heffernan