5
votes

I'm trying to optimize the size of my Delphi classes so that they take up as less memory as possible cause I'm creating a great number of them.

The thing is, the classes themselves are pretty small but they aren't taking the space I was expecting. For example if I have

type MyClass = class
  private
    mMember1 : integer;
    mMember2 : boolean;
    mMember3 : byte;
end;

I will expect it to use 6 bytes, but, due to alignment it ends up using 12 bytes, that's booleans use up 4 bytes instead of 1 byte... and the same goes for the byte field...

For records you can either use the {$A1} directive or declare it as a packed record to make it use up just the needed memory.

Is there any way to make the same thing with classes? (Maybe some tutorial on how to correctly override NewInstance class method?)

Edit: Ok, a little explanation about what I'm doing...

First, real class size is something like 40 bytes including the space taken up by the VMT and the Interface pointers.

The classes all inherit from a base RefCounting class whose size is 8 bytes (an integer FRefCount and some methods to allow reference counting) and they MUST support interfaces (hence not using packed records at all).

This objects get passed around and being casts to several things, whithout the handlers knowing what they got. For example, I've got a class that receives a List of TItems and does something like:

if Supports(List[i], IValuable, IValInstance) then
  Eval(IValInstance.Value);

then another handler may check for other interface

If Supports(List[i], IStringObject, IStringInstance) then
  Compose(IStringInstance.Value)

That way the List gets treated different by each Handler...

About how I get the total size of the class I'm using a modified Memory Manager so that I can keep track of how much memory the "real" memory manager uses up for the class. In that way I'm pretty confident instances are not being packed.

Finally this is in Delphi 7. I've tried to use the {$A1} precompiler directive with no luck, fields get aligned any way, and I may have several million instances as a worst case scenario so saving 6 bytes can result on several MB being saved.

9
What happens if you put the keyword "packed" in front of class? (which it looks like you've left out of the code there)Lasse V. Karlsen
Compiler error :P you can just use packed with recordsJorge Córdoba
How do you measure exactly? Seeing as my calculation for that class is 6 bytes for the fields, and packed does work on my end, could depend on compiler version and whether you're compiling for .NET or not? Mine is Delphi 2007, no .NETLasse V. Karlsen
I've expanded WHY I'm doing what I'm doing and why packed records are not an option. Sorry I made it confusing the first time. The number where just an example to illustrate the point, of course you have to take into account the space of the Virtual Method Table as well as any interface pointers present in the class.Jorge Córdoba
Jorge, you have misunderstood. If you put all your fields into a packed record, that does not preclude you from using interfaces. The compiler address the interface fields for you automatically when it defines the class's layout. A record field does not interfere with that. Besides, interface fields don't affect alignment since they're four-byte values anyway. You don't need a special memory manager to measure this, either; just use the InstanceSize function on the class.Rob Kennedy

9 Answers

10
votes

You could use a packed record as a field of your objects:

type
  TMyRecord = packed record
    Member1 : integer;
    Member2 : boolean;
    Member3 : byte;
  end;

  TMyClass = class
  private
    FData : TMyRecord;
   function GetMember1 : Integer;
  public
    property Member1 : Integer read GetMember1;
    // Later versions of Delphi allow "read FData.Member1;", not sure when from
  end;

function TMyClass.GetMember1 : integer;
begin
  result := FData.Member1;
end;
5
votes

If you're worried so much about a few bytes (you mentioned 6 vs 12), you shouldn't be using a class at all. Use a record instead. You can then use packed to eliminate the alignment waste; however, be prepared to take a performance hit, as the default "non-packed" alignment is set up for the fastest access by the CPU.

4
votes

Why not just use packed records to begin with? It would leave out the overhead (slight) caused by descending from TObject...

4
votes

Absolutely. You can pack sets, arrays, records, objects and file types. Note that using packed does cause a slow down when accessing data and can cause some issues with type compatibility.

I tried this out in Delphi 2006. The editor's syntax checking flagged it as an error but it compiled just fine.

According the Delphi documentation the $A switch applies to class types as well as record types.

Update:

I tried this out in Delphi 6 as well. It compiles successfully. If packed classes won't compile in Delphi 7 you may have discovered a bug. If it is a bug its unlikely Embarcadero will do anything about it unless it still occurs in the latest version of Delphi, which doesn't appear to be the case.

3
votes

Maybe bit offtopic, but I've struggled with this before (pre D2006, so no records) for some ORM framework. Assuming that the "class" stuff is set in stone:

Tips and hints:

  1. the packing problem I worked around by having getters and setters for the fields, storing them in the array of byte of the class. Could even be bitpacked. If setters/getters are inlinable (then not an option for me, D6) it could be fairly cheap even.
  2. try to harvest heap allocation (both administrative overhead and slack space) overhead by initializing a block of memory yourself, setting the VMT and call the constructor on it. IIRC heap overhead was 8 bytes and the granularity of allocation of the old heapmgr was 8 byte, and with fastmm 16 byte. If you sort the classes according to size, you can use a bitmap as allocation structure
  3. If you are particularly evil, remember that a pointer has 2 or 3 bits slack. I used these bits as identity for an extremely much used type of allocation, saving the 4 byte the heap reserves to store size.
  4. Pay attention to your indexes. If you get a lot of objects (I had about 6 million), you have to be careful with your index types too. (no tstringlist please)
  5. Always keep the non obfuscated stuff under ifdef, for easier debugging testing (*)
  6. never use strings as key. Hash if necessarily. Normalizing structures is not only good for databases

(*) I later recompiled the "clean" version under 64-bit FPC , and it worked after a few minor sizeof(pointer()) despite the uglinesses of point 1 an 2

2
votes

Manually pack your data. Take every 4 bytes and put them in a single cardinal. If you have two short stings that are not multiple of 4 in length then put them into one short string and just read out the parts for each.

It would require a little more engineering on your part, to manually line everything up, but through the use of getters and setters the behavior will be transparent outside the class. You can come really close to the same results as the compiler packing it this way.

2
votes

If you are going to have an extremely large number of instances and want to avoid the overhead associated with individual allocations you can make use of packed records and maintain them externally from the class itself such as through a one or more large allocations of arrays.

Then, in the class you can store only one or two fields for indexing into the heap and offset. If you can get away with only a single large memory block you could reduce that to only the offset.

TPackedRecord = packed record ... end;
PPackedRecord = ^TPackedRecord;
TPackedRecordHeap = class
  ...
  function  Add: PPackedRecord;
  procedure Release( entry: PPackedRecord );
end;

TUsableClass = class
private
  heap: TPackedRecordHeap;
  data: PPackedRecord;
public
  constructor Create( heap: TPackedRecordHeap );
  ...
end;
1
votes

FYI, if that was a record it would be 8 bytes, and 6 bytes as a packed record. So you are looking at a 4 byte overhead for the class pointer (assuming you are in Delphi pre-2009) with a possibility of reclaiming 2 bytes if it were packed.

1
votes

I will expect it to use 6 bytes, but, due to alignment it ends up using 12 bytes

Even if you write "TMyClass = class end;" the class will inherit from TObject which has virtual methods.

That makes

  4 Bytes (VMT)
+ 4 Bytes (member1: Integer)
+ 1 Byte  (member2: Boolean)
+ 1 Byte  (member3: Byte);
+ 2 Bytes (alignment)
---------
 12 Bytes

So if you disabled the alignment, you will win only 2 Bytes.

By ordering the fields by there data type size (in the larger class that you mention) can eliminate some alignment holes. And $A- (Delphi 5) or $A1 (newer) doesn't work. Neither in Delphi 7 nor in Delphi 2009.

BTW: in Delphi 2009 you have additional 4 Bytes for the "Thread.Monitor" increasing the total class size to 16 Bytes.