views:

575

answers:

9

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.

+4  A: 

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

Chris Lively
+3  A: 

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.

codeelegance
I'll try it again in Delphi 7, but, having already tried it once, I'm pretty confident Delphi 7 just ignores the precompiler directive for classes... it's pretty frustrating...
Jorge Córdoba
You can have packed object types, but not packed classes. The compiler allows the syntax, but it doesn't have any effect. We're talking about classes, not old-style objects.
Rob Kennedy
+4  A: 

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.

Ken White
If his "great number of them" is a few million, then saving 50% of it is a big deal.
Mason Wheeler
If you need "a few million" instances of a class, then it's also worth considering the savings possible by not loading so many instances into memory in the first place.
Rob Kennedy
I agree with Rob. If you need a few *million* of anything, there's probably a better way of accomplishing what you're trying to do.@Mason: 50%, when it's 6 *bytes*, isn't a big deal. Even when you're talking quite a few. It's an integer and a half of space - remove a couple of unused variables or use Byte when you can instead of integer in a few places and you can save that much. I'm sure you've heard the term "premature optimization"...
Ken White
+1  A: 

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.

Jim McKeeth
+7  A: 

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;
Gerry
+1, but if the OP doesn't need the full range for all of the individual data elements he should use the technique outlined in http://stackoverflow.com/questions/282019 and fold the boolean into one bit of one of the other data elements, saving another byte in the process. On modern processors less memory consumption at the cost of more cpucycles is nearly every time a performance win as well.
mghie
Note also that you can put the whole packed record into TMyClass, creating a private unnamed type (because it is an implementation detail).
mghie
+2  A: 

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.

Jim McKeeth
+2  A: 

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

Marco van de Voort
Overriding NewInstance and FreeInstance may be helpful in doing point 2.
Rob Kennedy
+1  A: 

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.

Andreas Hausladen
+2  A: 

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;
Ryan VanIderstine