Thursday 10 December 2009

When to use record instead of classes

The original use or the record construct in ObjectPascal was to group different values in a way, that can be used in arrays, parameters etc. Classes have taken over this job, but the record construct is still sometimes important.

For instance:

type
  TTest=
    class
      a,b,c:integer;
    end;

var 
  arrT:array[1..1000000] of TTest;

begin
  for i:=low(arrT) to high(arrT) do begin
    arrT[i]:=TTest.Create;
    arrT[i].a:=2;
    arrT[i].b:=3;
    arrT[i].c:=4;
  end;
end;

On my computer, this takes 161ms first time, and 91ms second time. Reading it like this, takes 9ms:

begin
  c:=0;
  for i:=low(arrT) to high(arrT) do begin
    c:=c+arrT[i].a+arrT[i].b-arrT[i].c;
  end;
end;

If you rewrite this code to use array of record, it looks like this:

type
  RTest=
    record
      a,b,c:integer;
    end;

var 
  arrR:array[1..1000000] of RTest;

begin
  for i:=low(arrR) to high(arrR) do begin
    arrR[i].a:=2;
    arrR[i].b:=3;
    arrR[i].c:=5;
  end;
end;

This takes 17ms first time and 9ms second time. Reading it takes 6ms:

begin
  for i:=low(arrR) to high(arrR) do begin
    arrR[i].a:=2;
    arrR[i].b:=3;
    arrR[i].c:=5;
  end;
end;

In other words, in this specific case, a record is about 10 times faster than a class, using Delphi 2009. Adding a string that does not receive a value, does not change the values much. Instead of 17ms, it now takes 23ms, but the record size has increased, so that makes sense:

type
  RTest=
    record
      a,b,c:integer;
      s:string;
    end;

However, if you assign a value to the string, performance drops:

begin
  for i:=low(arrR) to high(arrR) do begin
    arrR[i].a:=2;
    arrR[i].b:=3;
    arrR[i].c:=5;
    arrR[i].s:='234';
  end;
end;

In this case, the record solution takes 124ms, and the class-based solution takes 214ms. In other words, record only beats the class solution by a factor 2. The reason? Each string value assignment is as complicated as creating an object.

Conclusion: record beats classes in speed, but the benefit is only significant if you use less than one string value per record, and don't use classes inside the record. The biggest improvement is in creating the data structures, not in reading them.

12 comments:

Alin said...

interesting

thank you

LachlanG said...

I like to use records rather than classes in situations where I'm storing only simple types and I don't want to be bothered with calling constructors and destructors.

The new Delphi 2010 TRttiContext is a bit like this. It's a record so you don't have to worry about freeing it after you're done with it.

Jørn E. Angeltveit said...

It's actually not that surprising. The key difference between records and classes is the memory management. (Beside the fact that records can't take advantage of OO techniques like inheritance, polymorphism etc)

The record type variables uses the local memory - the stack, whereas the class type variables needs to allocate memory dynamically on the heap.

Have you tested the speed when passing records vs classes as parameters (to procedures or functions). Classes will probably have the advantage here, since classes are passed as references, while records (by default) are passed as values (and thereby needs to be copied).

Unknown said...

Very useful article, thank you.

Have you tried any other data types in the class / record?

Lars D said...

I made these benchmarks, because I wanted to know the actual difference in speed more precisely, for a specific performance optimization that I was considering. I did not make other benchmarks.

Jørn E. Angeltveit said...

You conclude that Strings in records make the record slower. I think you can avoid this by setting the string length (i.e. s:String[255];). Long strings are also allocated dynamically...

Soul Intruder said...

Cool that you've blogged about it.
Some people do not understand that NOT every damn thing must be an object.

This hurts performance, and hurts badly especialy in algorithmic problems.

Krystian said...

There is a 'bug' in this case test. You are comparing time of creating classes + fill data, with filling records (no record create involved!).

In this benchmark, array of record (memory block of 1000000*3*SizeOf(Integer) bytes) are created as one single block of memory.

It should be benchamrked as below (creating + filling records):

type
PMyTestRecord = ^TMyTestRecord;
TMyTestRecord = record
a, b, c:integer;
end;

var
arrTP:array[1..1000000] of PMyTestRecord; // array of pointers to record, similar as an array of classes (also just pointers to classes)

procedure ...;
var
i: Integer;
begin
// benchmark timer start
for i:=low(arrTP) to high(arrTP) do
begin
New(arrTP[i]); // creating record, this is not managed record - no dynamic types, like String/array of/interfaces/WideStrings, it's only allocating a block of memory without clearing it, so this line is equal to: GetMem(arrTP[i], SizeOf(TMyTestRecord));

arrTP[i]^.a:=2; // without dereference "^" it works too, but I prefer that way :)
arrTP[i]^.b:=3;
arrTP[i]^.c:=4;
end;
// benchmark timer stop

// free records... etc.
for i:=low(arrTP) to high(arrTP) do
Dispose(arrTP[i]);
end;

Of course it will be faster than creating classes (it will be then around 2-5 times faster, but no like 10x), because class instance must be cleared and some procedures called (InitInstance, AfterConstruction, etc).

Also empty class instance 'eats' 8b of memory (D2009+ because of 'Hidden field' - System.TMonitor support, in earlier versions of delphi it was only 4bytes-pointer to VMT).
So simple class with one Integer field takes 12bytes, but record with one Integer field takes just 4 byets (but for such small memory block there is no difference, each of them gets overhead of MM/FastMM, and in FastMM 4 bytes and 12 bytes takes same block size - this probably changed in latest version).

stanleyxu (2nd) said...

good to know.

there are something to note:
1. you can consider a record as an array for different data types.
2. you get a free clone feature by using a record automatically (in compare with a class)
3. your demo code looks ugly... (begin not in next line) ;-)

Lars D said...

@Krystian: That is not a bug, it is fully intentional. The purpose was to maximize speed by using array of record, and not array of pointer to record.

Anonymous said...

Records usually created on the stack (as in original post). Classes always created on the heap (i.e. in memory which allocated dynamically).

Records has no memory overhead, classes added 8b per instance (as noted by Krystian).

Creating non managed records is free. Classes do a lot of job in 'background' (clear memory, calling InitInstance, AfterConstruction etc. In detail).

There is additional penalty while accessing class members (indirect reference through pointer).

In some cases records can be much faster than classes. And you should to known in which cases use records.

Unknown said...

Do keep in mind though, that because a record isn't a class, if you for instance have a function that returns a record from an array - you will not get a pointer of that record, as you would get to a class.

A := FuncToGetRec(21);
SetRecValue(21, 'x');
// A.Value is still the old value