- Mastering Delphi Programming:A Complete Reference Guide
- Primo? Gabrijel?i?
- 1086字
- 2021-06-24 12:33:39
Dynamic record allocation
While it is very simple to dynamically create new objects—you just call the Create constructor—dynamic allocation of records and other data types (arrays, strings ...) is a bit more complicated.
In the previous section, we saw that the preferred way of allocating such variables is with the New method. The InitializeFinalize demo shows how this is done in practice.
The code will dynamically allocate a variable of type TRecord. To do that, we need a pointer variable, pointing to TRecord. The cleanest way to do that is to declare a new type PRecord = ^TRecord:
type
TRecord = record
s1, s2, s3, s4: string;
end;
PRecord = ^TRecord;
Now, we can just declare a variable of type PRecord and call New on that variable. After that, we can use the rec variable as if it was a normal record and not a pointer. Technically, we would have to always write rec^.s1, rec^.s4 and so on, but the Delphi compiler is friendly enough and allows us to drop the ^ character:
procedure TfrmInitFin.btnNewDispClick(Sender: TObject);
var
rec: PRecord;
begin
New(rec);
try
rec.s1 := '4';
rec.s2 := '2';
rec.s4 := rec.s1 + rec.s2 + rec.s4;
ListBox1.Items.Add('New: ' + rec.s4);
finally
Dispose(rec);
end;
end;
Another option is to use GetMem instead of New, and FreeMem instead of Dispose. In this case, however, we have to manually prepare allocated memory for use with a call to Initialize. We must also prepare it to be released with a call to Finalize before we call FreeMem.
If we use GetMem for initialization, we must manually provide the correct size of allocated block. In this case, we can simply use SizeOf(TRecord).
We must also be careful with parameters passed to GetMem and Initialize. You pass a pointer (rec) to GetMem and FreeMem and the actual record data (rec^) to Initialize and Finalize:
procedure TfrmInitFin.btnInitFinClick(Sender: TObject);
var
rec: PRecord;
begin
GetMem(rec, SizeOf(TRecord));
try
Initialize(rec^);
rec.s1 := '4';
rec.s2 := '2';
rec.s4 := rec.s1 + rec.s2 + rec.s4;
ListBox1.Items.Add('GetMem+Initialize: ' + rec.s4);
finally
Finalize(rec^);
FreeMem (rec);
end;
end;
This demo also shows how the code doesn't work correctly if you allocate a record with GetMem, but then don't call Initialize. To test this, click the third button (GetMem). While in actual code the program may sometimes work and sometimes not, I have taken some care so that GetMem will always return a memory block which will not be initialized to zero and the program will certainly fail:
It is certainly possible to create records dynamically and use them instead of classes, but one question still remains—why? Why would we want to use records instead of objects when working with objects is simpler? The answer, in one word, is speed.
The demo program, Allocate, shows the difference in execution speed. A click on the Allocate objects button will create ten million objects of type TNodeObj, which is a typical object that you would find in an implementation of a binary tree. Of course, the code then cleans up after itself by destroying all those objects:
type
TNodeObj = class
Left, Right: TNodeObj;
Data: NativeUInt;
end;
procedure TfrmAllocate.btnAllocClassClick(Sender: TObject);
var
i: Integer;
nodes: TArray<TNodeObj>;
begin
SetLength(nodes, CNumNodes);
for i := 0 to CNumNodes-1 do
nodes[i] := TNodeObj.Create;
for i := 0 to CNumNodes-1 do
nodes[i].Free;
end;
A similar code, activated by the Allocate records button creates ten million records of type TNodeRec, which contains the same fields as TNodeObj:
type
PNodeRec = ^TNodeRec;
TNodeRec = record
Left, Right: PNodeRec;
Data: NativeUInt;
end;
procedure TfrmAllocate.btnAllocRecordClick(Sender: TObject);
var
i: Integer;
nodes: TArray<PNodeRec>;
begin
SetLength(nodes, CNumNodes);
for i := 0 to CNumNodes-1 do
New(nodes[i]);
for i := 0 to CNumNodes-1 do
Dispose(nodes[i]);
end;
Running both methods shows a big difference. While the class-based approach needs 366 ms to initialize objects and 76 ms to free them, the record-based approach needs only 76 ms to initialize records and 56 to free them. Where does that big difference come from?
When you create an object of a class, lots of things happen. Firstly, TObject.NewInstance is called to allocate an object. That method calls TObject.InstanceSize to get the size of the object, then GetMem to allocate the memory and in the end, InitInstance which fills the allocated memory with zeros. Secondly, a chain of constructors is called. After all that, a chain of AfterConstruction methods is called (if such methods exist). All in all, that is quite a process which takes some time.
Much less is going on when you create a record. If it contains only unmanaged fields, as in our example, a GetMem is called and that's all. If the record contains managed fields, this GetMem is followed by a call to the _Initialize method in the System unit which initializes managed fields.
The problem with records is that we cannot declare generic pointers. When we are building trees, for example, we would like to store some data of type T in each node. The initial attempt at that, however, fails. The following code does not compile with the current Delphi compiler:
type
PNodeRec<T> = ^TNodeRec<T>;
TNodeRec<T> = record
Left, Right: PNodeRec<T>;
Data: T;
end;
We can circumvent this by moving the TNodeRec<T> declaration inside the generic class that implements a tree. The following code from the Allocate demo shows how we could declare such internal type as a generic object and as a generic record:
type
TTree<T> = class
strict private type
TNodeObj<T1> = class
Left, Right: TNodeObj<T1>;
Data: T1;
end;
PNodeRec = ^TNodeRec;
TNodeRec<T1> = record
Left, Right: PNodeRec;
Data: T1;
end;
TNodeRec = TNodeRec<T>;
end;
If you click the Allocate node<string> button, the code will create a TTree<string> object and then create 10 million class-based nodes and the same amount of record-based nodes. This time, New must initialize the managed field Data: string but the difference in speed is still big. The code needs 669 ms to create and destroy class-based nodes and 133 ms to create and destroy record-based nodes.
Another big difference between classes and records is that each object contains two hidden pointer-sized fields. Because of that, each object is 8 bytes larger than you would expect (16 bytes in 64-bit mode). That amounts to 8 * 10,000,000 bytes or a bit over 76 megabytes. Records are therefore not only faster but also save space!