Page 1 of 3 123 LastLast
Results 1 to 10 of 25

Thread: CLASS vs. OBJECT and memory management

  1. #1

    CLASS vs. OBJECT and memory management

    I'm working in an old-school 2D game engine (much inspired by Action Arcade Adventure Set) using Allegro.pas (Spam? What Spam? ). I have some stuff done, but now that I've finished with Sprites and before to start with the tile-map stuff I start to concern with memory management.

    You know, FPC has two ways to define objects: CLASS and OBJECT. Main difference between CLASS and OBJECT is that CLASS is always referenced and must be created explicitly as it is always dynamic while you can created OBJECT “statically” as you do with RECORD. So let's get the Sprite example. Using CLASS you create a list of sprites this way:

    Code:
    VAR
      Sprites: ARRAY OF TSprite;
    BEGIN
      SetLength (Sprites, NumSprites);
      FOR Ndx := LOW (Sprites) TO HIGH (Sprite) DO
        Sprites[Ndx] := Tsprite.Create;
    END;
    So you have a list of pointers to the actual objects. Then you may end with the sprite list fragmented in the memory (and the more you add and remove sprites, the more fragmented would be).

    Using OBJECT:
    Code:
    VAR
      Sprites: ARRAY OF TSprite;
    BEGIN
      SetLength (Sprites, NumSprites);
      FOR Ndx := LOW (Sprites) TO HIGH (Sprite) DO
        Sprites[Ndx].Init;
    END;
    Despite you need to call method Init (mandatory in some cases), you have an actual list of sprites in a continuous memory block.

    I’m not an expert in current microprocessor architectures inner (I’ve stuck in the Z-80) but I know a little about caches, and I suspect that the CLASS approach would be a bottleneck when executing next code:
    Code:
    { Update and render sprites }
      FOR Ndx := LOW (Sprites) TO HIGH (Sprite) DO
      BEGIN
        Sprites[Ndx].Update (DeltaTime);
        Sprites[Ndx].PutSprite
      END;
    An array of OBJECTs may be faster as the whole ARRAY may be uploaded to the microprocessor cache at once, or in runs of n sprites. The array of CLASS references would be much slower if objects are far from the ARRAY forcing to upload cache in each loop iteration.

    Wy the way, using an array of OBJECTs prevents you to use virtual methods, as all OBJECT on the array will be the same type. This can be fixed by using pointers to procedures or event properties instead; as far as I know there’s a microprocessor cache for data and another one for code, so performance won’t be affected so much. This is a bit ugly and can be implemented by simple RECORDs too, but CLASS actually uses that approach internally (the virtual table is just an array of pointers to method) just with compiler magic.

    Another bad thing is that inserting and removing sprites will be ugly. Also if you do it dynamically it would take much time to resize the ARRAY. Actually I'm using a linked list implementation right now, but Build engine uses a fixed sized array for data, including sectors and sprites, then uses several internal linked lists to organize them (for example, one linked list for each map sector that links all sprites that are inside sector); most games that use Build add and remove sprites (bullet holes, explossions, smoke…) and Duke Nukem 3D worked quite well in my old 486.

    Am I right in my thoughts? Are OBJECT worth of? Does Delphi support OBJECTs too? How much microprocessor caches affect performance? Does a fixed size array of OBJECT counts as pool?
    Last edited by Ñuño Martínez; 13-06-2016 at 11:23 AM.
    No signature provided yet.

  2. #2
    In Delphi Objects are still available but are only meant for backward compatibility and their use is therefore not recommended.

    From Delphi documentation:
    Object types are supported for backward compatibility only. Their use is not recommended.
    Perhaps you might consider using what I call a "fake class approach". In this approach you are actually storing your data in array of records but instead of accessing that data directly from records you are accessing them with properties of a standardized class.
    Using this approach you have all of your data tightly stored in continuous memory blocks but still use advantages of classes. Here is a quick code example:

    Code:
    type
      TDirection = (dirLeft, dirRight);
    
      //Record definition for storing data of a sprite
      RSprite = record
        XPos: Single;
        YPos: Single;
        Width: Integer;
        Height: Integer;
        ImageIndex: Integer;
      end;
    
      //Array of sperite records to keep data stored in one continoud block of memory
      ASprites = Array of RSprite;
    
      //You can even split your data to multiple arrays keeping specific kinds of data
      //like position, helth status, etc.
      //This means that if for instance you need to loop through all characters to see
      //if any of them should be dead (zero or negative health) only data related to
      //health would be put on stack
      RHealthStatus = record
        HealthPoints: Integer;
        MaxHealthPoints: Integer;
        RegenerationRate: Integer;
      end;
    
      //Array of records containing unit health data
      AHelthStatus = Array of RHealthStatus;
    
      //Specialized class for accessing data.
      //Note that all properties are indexed properties since you need to tell which
      //sprite are you acessing
      TSprites = class(TObject)
      private
        FSprites: ASprites;
      protected
        function GetSpritesCount: Integer;
        function GetXPos(Index: Integer): Single;
        function GetYPos(Index: Integer): Single;
        function GetWidth(Index: Integer): Integer;
        function GetHeight(Index: Integer): Integer;
        function GetImageIndex(Index: Integer): Integer;
        procedure SetXPos(Index: Integer; const AXPos: Single);
        procedure SetYPos(Index: Integer; const AYPos: Single);
        procedure SetWidth(Index: Integer; const AWidth: Integer);
        procedure SetHeight(Index: Integer; const AHeight: Integer);
        procedure SetImageIndex(Index: Integer; const AImageIndex: Integer);
      public
        procedure Add(ASprite: RSprite);
        procedure Remove(SpriteIndex: Integer);
        constructor Create(AInitialSize: Integer);
        property SpritesCount: Integer read GetSpritesCount;
        property XPos[Index: Integer]: Single read GetXPos write SetXPos;
        property YPos[Index: Integer]: Single read GetYPos write SetYPos;
        property Width[Index: Integer]: Integer read GetWidth write SetWidth;
        property Height[Index: Integer]: Integer read GetHeight write SetHeight;
        property ImageIndex[Index: Integer]: Integer read GetImageIndex write SetImageIndex;
      end;
    
      //You can even subclass the base sprite class for adding additional functionality
      TMovableSprite = class(TSprites)
      protected
        function Move(Distance: Integer; Direction: TDirection): Boolean;
        //Inherited properties
        property SpritesCount;
        property XPos;
        property YPos;
        property Width;
        property Height;
        property ImageIndex;
      end;
    
      //You can even acces data from different specialized arrays of records
      TPlayer = class(TMovableSprite)
      private
        FHealthStatus: RHealthStatus;
      protected
        function GetHealthPoints: Integer;
        function GetMaxHealthPoints: Integer;
        function GetRegenerationRate: Integer;
        procedure SetHealthPoints(const AHealthPoints: Integer);
        procedure SetMaxHealthPoints(const AMaxHealthPoints: Integer);
        procedure SetRegenerationRate(const ARegenerationRate: Integer);
      public
        property HealthPoints: Integer read GetHealthPoints write SetHealthPoints;
        property MaxHealthPoints: Integer read GetMaxHealthPoints write SetMaxHealthPoints;
        property RegenarationRate: Integer read GetRegenerationRate write SetRegenerationRate;
        //Inherited properties
        property SpritesCount;
        property XPos;
        property YPos;
        property Width;
        property Height;
        property ImageIndex;
      end;
    
    implementation
    
    { TSprites }
    
    procedure TSprites.Add(ASprite: RSprite);
    begin
      SetLength(FSprites,Length(FSprites)+1);
    end;
    
    constructor TSprites.Create(AInitialSize: Integer);
    begin
      SetLength(FSprites, AInitialSize);
    end;
    
    function TSprites.GetHeight(Index: Integer): Integer;
    begin
      result := FSprites[Index].Height;
    end;
    
    function TSprites.GetImageIndex(Index: Integer): Integer;
    begin
      result := FSprites[Index].ImageIndex;
    end;
    
    function TSprites.GetSpritesCount: Integer;
    begin
      result := Length(FSprites);
    end;
    
    function TSprites.GetWidth(Index: Integer): Integer;
    begin
      result := FSprites[Index].Width;
    end;
    
    function TSprites.GetXPos(Index: Integer): Single;
    begin
      result := FSprites[Index].XPos;
    end;
    
    function TSprites.GetYPos(Index: Integer): Single;
    begin
      result := FSprites[Index].YPos;
    end;
    
    procedure TSprites.Remove(SpriteIndex: Integer);
    begin
      //code for removing object from array
    end;
    
    procedure TSprites.SetHeight(Index: Integer; const AHeight: Integer);
    begin
      FSprites[Index].Height := AHeight;
    end;
    
    procedure TSprites.SetImageIndex(Index: Integer; const AImageIndex: Integer);
    begin
      FSprites[Index].ImageIndex := AImageIndex;
    end;
    
    procedure TSprites.SetWidth(Index: Integer; const AWidth: Integer);
    begin
      FSprites[Index].Width := AWidth;
    end;
    
    procedure TSprites.SetXPos(Index: Integer; const AXPos: Single);
    begin
      FSprites[Index].XPos := AXPos;
    end;
    
    procedure TSprites.SetYPos(Index: Integer; const AYPos: Single);
    begin
      FSprites[Index].YPos := AYPos;
    end;
    
    { TMovableSprite }
    
    function TMovableSprite.Move(Distance: Integer; Direction: TDirection): Boolean;
    begin
      //Code for moving sprites
    end;
    
    { TPlayer }
    
    function TPlayer.GetHealthPoints: Integer;
    begin
      result := FHealthStatus.HealthPoints;
    end;
    
    function TPlayer.GetMaxHealthPoints: Integer;
    begin
      result := FHealthStatus.MaxHealthPoints;
    end;
    
    function TPlayer.GetRegenerationRate: Integer;
    begin
      result := FHealthStatus.Regenerationrate;
    end;
    
    procedure TPlayer.SetHealthPoints(const AHealthPoints: Integer);
    begin
      if AHealthPoints > FHealthStatus.MaxHealthPoints then
        FHealthStatus.HealthPoints := FHealthStatus.MaxHealthPoints
      else if AHealthPoints < 0 then
        FHealthStatus.HealthPoints := 0
      else
        FHealthStatus.HealthPoints := AHealthPoints;
    end;
    
    procedure TPlayer.SetMaxHealthPoints(const AMaxHealthPoints: Integer);
    begin
      FHealthStatus.MaxHealthPoints := AMaxHealthPoints;
    end;
    
    procedure TPlayer.SetRegenerationRate(const ARegenerationRate: Integer);
    begin
      FHealthStatus.Regenerationrate := ARegenerationRate;
    end;
    If you decide to go for splitting data into multiple arrays you need to make sure all arrays have same order of items (common index).

    This approach comes most useful when you are creating and destroying objects quote often since you don't suffer from class creation/destruction overhead.
    Another or its advantages is that because you are storing your data in arrays which are continuous memory blocks you can save them to or load them from files as such instead of needing to loop through a bunch of classes and save/load data for each of them separately.

    Notable drawbacks is the fact that if you store all the data in a single array the records that must be of the same type must have all the fields for all the data that most complex object stored in that array would need. This would mean that you might be wasting some space with more simpler objects whose data would not fill all the records fields.
    And if you go for multiple arrays approach maintaining data in them becomes more difficult.
    And probably the biggest drawback of such approach would be hard way to achieving good data reusability, even thou it is still doable.

    If you need to discus some more details of this design please let me know.
    Last edited by SilverWarior; 13-06-2016 at 06:58 PM.

  3. #3
    Quote Originally Posted by Ñuño Martínez View Post
    By the way, using an array of OBJECTs prevents you to use virtual methods, as all OBJECT on the array will be the same type.
    Not really. When using virtual methods, each object must also contain a "secret" member that points to the VMT. Setting up this pointer is done by the constructor, so if you typecast some of the objects in the array and call the child constructor on them, the VMT will be properly initialized to that of the child class. One important thing to note here, though, is that the descendant classes cannot declare any new fields, since they would be located after the base class data, thus accessing them would go past the base class memory area and mess stuff up in the next array member.

  4. #4
    Quote Originally Posted by SilverWarior View Post
    (...)
    I think I don't understand your code. Extending TSprites will duplicate the sprite list, and I don't get benefits. And it's worst with the TPlayer class. I mean, create a "TPlayer" object will create a new list of sprites, doesn't it?

    Quote Originally Posted by Super Vegeta View Post
    Not really. When using virtual methods, each object must also contain a "secret" member that points to the VMT. Setting up this pointer is done by the constructor, so if you typecast some of the objects in the array and call the child constructor on them, the VMT will be properly initialized to that of the child class. One important thing to note here, though, is that the descendant classes cannot declare any new fields, since they would be located after the base class data, thus accessing them would go past the base class memory area and mess stuff up in the next array member.
    You're right. If descendant classes use the same data than the parent ones then there's no problems. I see it. The problem is how to know which typecast to use. May be a "SpriteType" field will do the work but I find it quit ugly:
    Code:
      FOR Ndx := LOW (SpriteList) TO HIGH (SpriteList) DO
        CASE SpriteList[Ndx].SprType OF
          stPlayer : TSprPlayer (SpriteList[Ndx]).Update;
          stBullet : TSprBullet (SpriteList[Ndx]).Update;
          stEnemy1 : TSprEnemy1 (SpriteList[Ndx]).Update;
          stEnemy2 : TSprEnemy2 (SpriteList[Ndx]).Update;
          ELSE SpriteList[Ndx].Update;
        END;
    Using a "CLASS OF ..." field may fix the problem, but I think it's not possible...
    Code:
      TYPE
        TSprite = CLASS; { Forward declaration. }
    
        TSpriteClass = CLASS OF TSprite;
    
        TSprite = CLASS (TObject)
        PRIVATE
        { ... }
          fSpriteClass: TSpriteClass;
        PUBLIC
        { ... } 
    
          PROPERTY SprClass: TSpriteClass READ fSpriteClass;
        END;
    
    { ... }
        FOR Ndx := LOW (SpriteList) TO HIGH (SpriteList) DO
        { This doesn't work. }
          SpriteList[Ndx].SprClass (SpriteList[Ndx]).Update;
    Anyway, I'm reading Game Programming Patterns (you should read it even if you don't like patterns, actually I don't like them but the book gives a lot of good ideas) and I've found some information. What I'm asking for is an Object Pool, as you have all objects and reuses them when needed. This is the way Build works, for example.

    Also I've found a problem about microprocessor caches: if using a scripting (bytecode ) to define the behavior of sprites (allowing a way to keep size of all TSprite while extending them), anytime a script is executed it may kill the cache.
    No signature provided yet.

  5. #5
    Quote Originally Posted by Ñuño Martínez View Post
    If descendant classes use the same data than the parent ones then there's no problems. I see it. The problem is how to know which typecast to use.
    Unless you want to dynamically switch an array member between types, then you can just call the adequate constructor when creating the member and it will work fine.
    Code:
    program aaa; {$MODE OBJFPC}
    
    type bbb = object 
    	public procedure hello; virtual;
    	public constructor create;
    	public destructor destroy; virtual;
    end;
    
    type ccc = object(bbb) 
    	public procedure hello; virtual;
    	public destructor destroy; virtual;
    end;
    
    procedure bbb.hello;
    begin
    	writeln('bbb: hello')
    end;
    
    constructor bbb.create; 
    begin
    	writeln('bbb: create')
    end;
    
    destructor bbb.destroy;
    begin
    	writeln('bbb: destroy')
    end;
    
    
    procedure ccc.hello;
    begin
    	writeln('ccc: hello')
    end;
    
    destructor ccc.destroy;
    begin
    	writeln('ccc: destroy')
    end;
    
    
    Var
    	X: Array[0..1] of bbb;
    
    begin
    	X[0].Create();
    	ccc(X[1]).Create();
    	
    	X[0].Hello();
    	X[1].Hello();
    	
    	X[0].Destroy();
    	X[1].Destroy()
    end.
    Although the compiler says that "X is not initialized" during "X[0].Create()", the above code (with FPC 2.6.4) works fine - after the constructors are called, the VMT pointers are set up, so calls to Hello() and Destroy() are properly resolved to those of the child class:
    Code:
    bbb: create
    bbb: create   // "ccc" did not have an explicit constructor, so the "bbb" constructor was called, but the VMT is still set up properly
    bbb: hello
    ccc: hello
    bbb: destroy
    ccc: destroy
    IMO, with what you're trying to achieve, it would actually be simpler to just use records and have a "spriteType" field there. You can use the Case() statement to test for the type and call the appropriate procedure when needed. It may not look the prettiest, but it's the simplest way and there's no risk that compiler optimisations mess something up.
    Also, you can have a SPRITETYPE_NONE value for your spriteType, which gives you a way to have "NULL" array members for free.

    Also, have you tried using a profiler? It may turn out that the memory accesses aren't really that much of a problem and you're trying to optimize the wrong thing.
    Last edited by Super Vegeta; 15-06-2016 at 04:32 PM. Reason: Expand with note on using records

  6. #6
    Quote Originally Posted by Ñuño Martínez View Post
    I think I don't understand your code. Extending TSprites will duplicate the sprite list, and I don't get benefits.
    Not necessarily. It depends on how you are extending the TSprites class. If you are extending it by just adding additional methods to descendant class like in TMovableSprite it won't duplicate list since you are still accessing it through inherited methods or properties from parent class.

    Quote Originally Posted by Ñuño Martínez View Post
    And it's worst with the TPlayer class. I mean, create a "TPlayer" object will create a new list of sprites, doesn't it?
    Yes in case of TPlayer I do create a new array to store additional information that is not being stored in ASprites array.

    What is advantage of this?

    For instance during rendering phase you only need information about sprites positions, size and orientation. You don't need additional information that you might be storing in your objects like in-game unit health.
    Now if you are storing all that information in one array when the data from that array is put on stack the stack would contain the data that you need during the rendering phase and also the data that you don't need during rendering phase. So you will be able to fill the stack with information for limited number of units at once.
    But if you have one array which basically contains only data that that is needed during rendering phase you would be able to store relevant data for greater number of in-game units at once.

    On the other hand in a different part of your code you might only need data related to in-game units health (checking which units are dead, regenerating their health). Here it would also be beneficial if you only put relevant data about units health on the stack without other unneeded data like unit positions etc.

    Perhaps it would be more understandable if I create a mind graph representation of the concept

  7. #7
    Quote Originally Posted by Super Vegeta View Post
    IMO, with what you're trying to achieve, it would actually be simpler to just use records and have a "spriteType" field there. You can use the Case() statement to test for the type and call the appropriate procedure when needed. It may not look the prettiest, but it's the simplest way and there's no risk that compiler optimisations mess something up.
    Also, you can have a SPRITETYPE_NONE value for your spriteType, which gives you a way to have "NULL" array members for free.
    This is much like how Build works, and I think I'll go this way.

    Quote Originally Posted by Super Vegeta View Post
    Also, have you tried using a profiler? It may turn out that the memory accesses aren't really that much of a problem and you're trying to optimize the wrong thing.
    No, I didn't. I tried to use GNU's profiler a long time ago (+5 years) and it didn't work too much as it couldn't identify several Object Pascal stuff. It was useless. I should try again, may be FPC 3.0 works. [edit] I've just read at the Lazarus wiki that "Support for using gprof under Linux is broken in FPC 2.2.0-2.4.0; it is fixed in FPC 2.6+", which explains why it didn't work.

    Quote Originally Posted by SilverWarior View Post
    Perhaps it would be more understandable if I create a mind graph representation of the concept
    That would be great because I'm a bit lost.
    Last edited by Ñuño Martínez; 16-06-2016 at 04:39 PM.
    No signature provided yet.

  8. #8
    Can I read this thread as allegory to Object vs Record (with methods)

  9. #9
    Of course you can.

    By the way, I've discovered "records with methods" recently. May be they are a better approach for what I want/need as they don't need initialization/constructor and no inheritance is involved (AFAIK FPC OBJECT inherits from an internal TObject the same way CLASS does, doesn't it?).
    Last edited by Ñuño Martínez; 19-06-2017 at 12:03 PM. Reason: typo
    No signature provided yet.

  10. #10
    You definitely should use records, with the {$modeswitch AdvancedRecords} define set. The classic Turbo Pascal style "objects" are well known to be buggy (memory leaks, e.t.c.) and as far as I know they aren't being actively maintained or developled in either Delphi or FPC, and I see no reason why they ever would be again in the future.

Page 1 of 3 123 LastLast

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •