Recent

Author Topic: TObject memory use  (Read 16100 times)

User137

  • Hero Member
  • *****
  • Posts: 1791
    • Nxpascal home
TObject memory use
« on: October 14, 2011, 10:51:07 pm »
I'm writing a particle engine and trying to figure out if i'd use TObject or record for each particle. I did a small test by observing task manager while running the app from explorer with different settings.

Code: [Select]
  // Empty memory use: 18540 (kilobytes)
  // Record memory use: 18748 (+208)
  // Class with 2 methods memory use: 18920 (+380)

  TParticle = record
  //TParticle = class
    x,y, ix,iy, r,g,b, a,ia, rad: single;
  //public
    //procedure SetVelocity(_ix, _iy: single);
    //procedure SetPosition(_x, _y: single);
  end;

  for i:=1 to 10000 do
    pt.Add2DParticle(0, 0);

The results were obvious; using records takes roughly half the memory objects use. If i kept adding more methods to the TParticle, the memory use only increased. This isn't even counting what .Create and .Free methods may be doing internally to consume CPU cycles at runtime.

It's a shame as i'd really like object oriented approach to this, such as base TParticle class from which i could derive TParticle2D and TParticle3D, maybe more kinds.

Troodon

  • Sr. Member
  • ****
  • Posts: 484
Re: TObject memory use
« Reply #1 on: October 15, 2011, 12:54:28 am »
Objects are allocated dynamically on the heap so memory will increase at each Add() step. You will notice the same memory increase if you store the records in a dynamic array, or any kind of pointer list (e.g., a linked list). If, on the other hand, you store your data in static array then it will be preallocated and memory usage will not increase that much at run time.
« Last Edit: October 15, 2011, 12:57:59 am by Troodon »
Lazarus/FPC on Linux

User137

  • Hero Member
  • *****
  • Posts: 1791
    • Nxpascal home
Re: TObject memory use
« Reply #2 on: October 15, 2011, 03:21:32 am »
In the previous post test my records are also in a dynamic array which calls setlength() on each add. I'm just saying that 1 TObject in general with same variables takes more memory space than 1 record.

Leledumbo

  • Hero Member
  • *****
  • Posts: 8836
  • Programming + Glam Metal + Tae Kwon Do = Me
Re: TObject memory use
« Reply #3 on: October 15, 2011, 04:59:34 am »
Quote
I'm just saying that 1 TObject in general with same variables takes more memory space than 1 record.
Of course, classes have more features than records (dynamic allocation, inheritance, etc.) so it's normal that they contain more (hidden) information than records.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12399
  • Debugger - SynEdit - and more
    • wiki
Re: TObject memory use
« Reply #4 on: October 15, 2011, 12:20:22 pm »
Quote from: User137
    In the previous post test my records are also in a dynamic array which calls setlength() on each add. I'm just saying that 1 TObject in general with same variables takes more memory space than 1 record.

You can reduce the memory overhead of objects, by writing your own memory allocation for them and holding several in a single block of memory (similar to records in a dynamic array).

To understand one of the differences between object and records:
- objects are internally pointer
- records are not pointers
- (dynamic array are pointers, static arrays are not. But that does not matter for now)

var
  AObj: TObject
  ARec: record (* fields .... *) end;

Lets say:
-  AObj (the actual variable) is locate in memory at 0x40000
  Then it takes sizeof(Pointer) = 4 bytes (on 32 bit CPU) at that location.
  Those 4 bytes point to 0x052100 where the actual data is located:
     sum of SizeOf(all_fields)
    + some internal data, like pointer to class info (class info exists only ONCE for all objects of the class)
- ARec (the actual variable) is located at 0x040010
  And it has all it's data right there

the above is also true, if your objects are in a dynamic array. The dynamic array, is an array of the pointers.

Since the obj has it's data at  0x052100, this memory had to be allocated. The heap mgr would have had to keep track of this allocation, and add some overhead (depends on which heap mgr you use). But usually at least the size of the memory (4 or 8 bytes / 32 or 64 bit CPU). And likely some other internal info. E.g allocating memory can fragment the amount of free memory. The heap mgr allocates big chunks from the os, and then divides them, but with fragmentation some of it can be lost.

So you can see: An object has an additional pointer (the actual variable) + pointer to class (since the actual class of your instance can be a sub-class of what the variable was declared as, so it must be stored in the data) + memory mgr overhead...

And maybe even more. I don't know.

Now if you allocate memory for your objects yourself, you can alocate a continues block of memory for 100 objects, saving 99 times the heap-mgr overhead.
(Don't ask me how to do. But it is possible. Be careful, as inherited classes need different amount of mem, making this extremely complex. I think you override GetInstance or something like that....)

Yet you get inheritance for this price...

P.s. accidentally modified by me(marcov) when I hit modify instead of quote. (stupid muscle memory). Restored the best I could
« Last Edit: October 15, 2011, 01:05:46 pm by marcov »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12896
  • FPC developer.
Re: TObject memory use
« Reply #5 on: October 15, 2011, 01:05:17 pm »
Since the obj has it's data at  0x052100, this memory had to be allocated. The heap mgr would have had to keep track of this allocation, and add some overhead (depends on which heap mgr you use). But usually at least the size of the memory (4 or 8 bytes / 32 or 64 bit CPU). And likely some other internal info. E.g allocating memory can fragment the amount of free memory. The heap mgr allocates big chunks from the os, and then divides them, but with fragmentation some of it can be lost.

Some small additions:
1. Heap granularity is usually 16-bytes in modern systems (due to SSE2 alignment requirements, to keep the head of the allocation aligned). Even if FPC doesn't do it yet, start calculating with that magnitude, since it should (specially for 64-bit). Usually this is the biggest factor for large amount of objects.
2. allocation size can be retrieved by calling ttypeofobject.instancesize
3. When looking for an example: Package fcl-xml used to have such beast (node manager or something like it)

User137

  • Hero Member
  • *****
  • Posts: 1791
    • Nxpascal home
Re: TObject memory use
« Reply #6 on: October 15, 2011, 01:59:03 pm »
Well, using records seems like the best idea at this point.

But it would be cool if freepascal could give more features to records. I can only fantasize about this:

Code: [Select]
type
  TRGB = packed record
    r, g, b: byte;
  public
    procedure SetColor(r, g, b: byte);
  end;

  TRGBA = packed record extends TRGB
    a: byte;
  public
    procedure SetColor(r, g, b, a: byte); override;
  end;
// Now TRGBA can be used as normal variable, taking total 32 bits of memory all in all.
// Or in a dynamic array. It just doesn't have or need a constructor.

Isn't this how C++ classes work? I don't know for sure though.

I mean, both TObject and Object types suffer from complications that are pointers and so forth. Records have none of that, clean and simple. Compiler would create identical code to procedural coding by this. I just don't understand why it hasn't been already implemented.
« Last Edit: October 15, 2011, 02:21:26 pm by User137 »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12896
  • FPC developer.
Re: TObject memory use
« Reply #7 on: October 15, 2011, 03:18:42 pm »
Well, using records seems like the best idea at this point.

But it would be cool if freepascal could give more features to records. I can only fantasize about this:

Inheritance is generally not possible without adding some overhead. This because the VMT must be found from a
class that is passed with polymorphism.

Old object formats like TP's tobject (that FPC and Delphi also support) and C++ try to hide this fact by only
adding the overhead when strictly needed. (when there are virtual methods).

However the FPC classes system is more advanced, allowing multiple ways to instantiate (see above), enumeration
in  for..in loops, handling exceptions in the constructor etc. For that virtual methods need to be in the base class.

Quote
Isn't this how C++ classes work? I don't know for sure though.

It is how OOP started. But we are now 20years hence :-)

Quote
I mean, both TObject and Object types suffer from complications that are pointers and so forth.

For most usage, that is a strength. Only for certain embedded cases that is an advantage, but since those are generally without inheritance, one could question if they need OOP at all.

Be careful with concepts "tobject" and "object", since their usage depends on the used mode. If they refer to TP Object, this is all not the case.

Quote
Records have none of that, clean and simple. Compiler would create identical code to procedural coding by this. I just don't understand why it hasn't been already implemented.

See TP objects, implemented since 1997 or so (and since 1990 by TP).

Be warned however, you are in for some pain. In general I would advise, if you don't need inheritance: use records. If you need inheritance: use classes and take the overhead.

Period.

User137

  • Hero Member
  • *****
  • Posts: 1791
    • Nxpascal home
Re: TObject memory use
« Reply #8 on: October 15, 2011, 04:06:27 pm »
Inheritance is generally not possible without adding some overhead. This because the VMT must be found from a
class that is passed with polymorphism.
You mean this http://wiki.freepascal.org/Compiler_generated_data_and_data_structures

Now, take this just as brainstorming, i'm not really trying to force record procedures if they are impossibility...

Actually as far as i can think, record based procedure would still not need such VMT. Compiler should be able to see the address of each variable and directly attach that in code. Lets try the earlier:

Code: [Select]
var rgb: TRGB;
rgb.SetColor(0, 128, 255);

procedure TRGB.SetColor(r, g, b: byte);
begin
  self.r:=r;
  self.g:=g;
  self.b:=b;
end;
Now, for programmer there is a clear "object" or variable however you call it, "self". But compiler actually doesn't need to track that. It has taken note on which record variable calls this procedure, and attaches its address in place of "self". This "tracking" is only done in the first phases of compilation and finally eliminated, making this do internally something like:
Code: [Select]
var rgbVar: TRGB;
rgbVar.SetColor(0, 128, 255);

procedure TRGB.SetColor(r, g, b: byte);
begin
  rgbVar.r:=r;
  rgbVar.g:=g;
  rgbVar.b:=b;
end;
Or maybe internally passing the variables pointer as first parameter. This is overhead, but still less than what's mentioned in the thread before:
Code: [Select]
procedure TRGB.SetColor(this: ^TRGB; r, g, b: byte);
Actually if you think about it in procedural way, this is perfectly valid way. Only difference is that it would be compiler that adds the first parameter, and that its declared inside the record type:
Code: [Select]
procedure RGBSetColor(this: ^TRGB; r, g, b: byte);
Now.. i don't know how restricted memory is between different procedures, or if this would mean allocating more memory to actually make instances of this procedure for every variable that uses it. That goes little beyond my understanding.
« Last Edit: October 15, 2011, 04:11:40 pm by User137 »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12896
  • FPC developer.
Re: TObject memory use
« Reply #9 on: October 15, 2011, 05:54:56 pm »
Now, take this just as brainstorming, i'm not really trying to force record procedures if they are impossibility...

Procedures in records is a Delphi feature, and FPC supports this in newer versions.

However, this is not OOP, since one of the requirements of OOP is polymorphism, and something like this doesn't support polymorphism. It is just procedures nested in a record namespace.

Despite being redundant for normal programming, this becomes useful with generics and for certain other newer language constructs. Which is why it got implemented in the end.

JuhaManninen

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4715
  • I like bugs.
Re: TObject memory use
« Reply #10 on: October 16, 2011, 12:28:51 pm »
Code: [Select]
...
  for i:=1 to 10000 do
    pt.Add2DParticle(0, 0);

The results were obvious; using records takes roughly half the memory objects use. If i kept adding more methods to the TParticle, the memory use only increased. This isn't even counting what .Create and .Free methods may be doing internally to consume CPU cycles at runtime.

It's a shame as i'd really like object oriented approach to this, such as base TParticle class from which i could derive TParticle2D and TParticle3D, maybe more kinds.

How many particles you need? Your example has 10 000 which is not much. 100 000 is not much either. Even a million is not much with today's computers.
A pointer takes 4 bytes (typically). Million pointers take 4 MB of memory. Even the cheapest laptops now have at least 1 GB of memory.
Or maybe your target is a limited embedded system, then your worries are more understandable.

Your extra pointer is not a relevant problem compared to systems with virtual machines and garbage collection. Most programming is now done with such systems (Java, .NET, all dynamic languages ...).
Object Pascal still generates optimized binary and handles memory efficiently. It is a good compromise between syntax clarity and code efficiency.

Juha
« Last Edit: October 16, 2011, 03:35:27 pm by JuhaManninen »
Mostly Lazarus trunk and FPC 3.2 on Manjaro Linux 64-bit.

User137

  • Hero Member
  • *****
  • Posts: 1791
    • Nxpascal home
Re: TObject memory use
« Reply #11 on: October 16, 2011, 04:35:07 pm »
4 extra bytes per particle isn't a problem, but as long as i keep adding new methods per particle the memory consumption grows exponentially, even if i don't add variables.

Oh, i also want to show you how it looks like at the moment:
http://www.youtube.com/watch?v=oa7vw20rcE8

TParticle = record returns sizeof() 48, so 10000 of them would take 480k memory just from variables, and this is general purpose particle, i could think of adding more options if i had made it into class. But particle-engine is freeing and creating particles alot of times per frame. I would imagine .Create method does also take some CPU time (i mean the base, empty constructor). In both cases the dynamic array is resized with setlength(), just records memory space is automatically allocated in that process.

In the meantime at least the particle engine itself is a class, and that is a relief.
« Last Edit: October 16, 2011, 05:37:30 pm by User137 »

Troodon

  • Sr. Member
  • ****
  • Posts: 484
Re: TObject memory use
« Reply #12 on: October 16, 2011, 05:06:09 pm »
Oh, i also want to show you how it looks like at the moment:
http://www.youtube.com/watch?v=oa7vw20rcE8

Great job! Keep it up.
Lazarus/FPC on Linux

JuhaManninen

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4715
  • I like bugs.
Re: TObject memory use
« Reply #13 on: October 16, 2011, 08:45:27 pm »
4 extra bytes per particle isn't a problem, but as long as i keep adding new methods per particle the memory consumption grows exponentially, even if i don't add variables.

Hmmm... maybe I misunderstood the sentence, but methods don't occupy memory at every object instance, they occupy memory once for a class definition.
Virtual methods add a VMT entry which takes a little more memory than normal methods, but still nothing like "memory consumption grows exponentially".
Can you please explain.

Quote
Oh, i also want to show you how it looks like at the moment:
http://www.youtube.com/watch?v=oa7vw20rcE8

Cool! Really.
I don't know much about graphics programming myself.


Quote
TParticle = record returns sizeof() 48, so 10000 of them would take 480k memory just from variables, and this is general purpose particle, i could think of adding more options if i had made it into class. But particle-engine is freeing and creating particles alot of times per frame. I would imagine .Create method does also take some CPU time (i mean the base, empty constructor). In both cases the dynamic array is resized with setlength(), just records memory space is automatically allocated in that process.

480k is nothing nowadays.
But yes, memory allocation done by constructor is always an "expensive" operation, even though the memory manager in FPC is well optimized (I have understood).
I think your problem is not memory consumption but creating and freeing many particles. How do you solve it with records? If you keep them all in a dynamic array, then you must somehow mark them as freed and later use the same space for a new particle. You could do the same thing with classes and objects by making your own particle-pool which reuses the already allocated particles.

I feel you are doing "premature optimization" now, concentrating on wrong things.
Did you try how much worse it actually is with classes and objects? If there is a problem with performance then you can optimize with object pools and such.

Things were different at Commodore 64 and Amiga times. People made amazing things with them and it really required optimized code because the resources were a small fraction of today's computers' resources.

Juha
Mostly Lazarus trunk and FPC 3.2 on Manjaro Linux 64-bit.

User137

  • Hero Member
  • *****
  • Posts: 1791
    • Nxpascal home
Re: TObject memory use
« Reply #14 on: October 16, 2011, 10:14:10 pm »
Its actually bit difficult to test or observe memory use for class objects. I do hope you are right that their definition counts just once. Only what i observed was that adding 2 simple setter procedures to the TParticle class, added program memory by tens of thousands of bytes.

I have debug info set to external file from compiler options but no optimizations, and i haven't tested it any more through than that, like with how much it increase changing particle count etc...

I know that 10000 particles would still work smoothly with class objects but for game programming every bit is important. It could mean a little framerate loss or time that could be used in calculating physics or something else. Even if you aim to make an engine that wouldn't require a top notch year 2011 computer, but work on a little older computers too.

When i delete a particle it does just this, moves last index in place of removed index, and that only works if i go the array through from "pCount-1 downto 0":
Code: [Select]
      if a<=0 then begin // a is particle transparency channel
        dec(pCount); pt[i]:=pt[pCount];
      end;

For texture array and some other classes i have earlier made this kind of procedure:
Code: [Select]
procedure TTextureSet.SetCount(n: integer);
begin
  count:=n; n:=nxMath.pow2fit(n);
  if n<>count2 then begin
    count2:=n; setlength(texture,count2);
  end;
end;
Basically it only calls setlength() for power of 2, only when the size must be increased. So if item count increases to 257 it will increase array to 512, and not increase that until item count reach 513, and then adds to 1024. I could use this for particles later possibly, but there is that little slowdown calling pow2fit(), but it would still be propably faster than calling setlength every time.

By the way, is that even beneficial for the dynamic memory manager or should i just increase it in like steps of 500 each time? It would at least count it way faster without need for logarithm functions. I mean, it just struck me 1 day that powers of 2 might feel "computer friendly numbers"  :D
« Last Edit: October 16, 2011, 10:25:44 pm by User137 »

 

TinyPortal © 2005-2018