Unittests pass, and then an invalid memory operation happens after?

Liam McGillivray yoshi.pit.link.mario at gmail.com
Wed Apr 3 21:57:00 UTC 2024


On Friday, 29 March 2024 at 01:18:22 UTC, H. S. Teoh wrote:
> Take a look at the docs for core.memory.GC.  There *is* a 
> method GC.free that you can use to manually deallocate 
> GC-allocated memory if you so wish.  Keep in mind, though, that 
> manually managing memory in this way invites memory-related 
> errors. That's not something I recommend unless you're adamant 
> about doing everything the manual way.

Was this function removed from the library? I don't see it in 
[the document](https://dlang.org/phobos/core_memory.html).

How is `GC.free` different from `destroy`?

I might take a look at it, but I'm not adamant about doing 
everything the manual way, so I probably won't use it very soon.

> I think you're conflating two separate concepts, and it would 
> help to distinguish between them.  There's the lifetime of a 
> memory-allocated object, which is how long an object remains in 
> the part of the heap that's allocated to it.  It begins when 
> you allocate the object with `new`, and ends with the GC finds 
> that it's no longer referenced and collects it.

No. I understand that when an object disappears from memory might 
happen after it disappears from the game. I'll explain later in 
this post why I wanted to remove the Unit object from memory when 
the unit dies.

> There's a different lifetime that you appear to be talking 
> about: the logical lifetime of an in-game object (not to be 
> confused with an "object" in the OO sense, though the two may 
> overlap).  The (game) object gets created (comes into existence 
> in the simulated game world) at a certain point in game time, 
> until something in the game simulation decides that it should 
> no longer exist (it got destroyed, replaced with another 
> object, whatever). At that point, it should be removed from the 
> game simulation, and that's probably also what you have in mind 
> when you mentioned your "die" function.

Yes; exactly. This was your hint that I'm not confusing these two 
things. Whether or not the unit object gets deleted, I need a 
function to remove it from the game on death.

The `die` function if I want the object to be destroyed on death:
```
void die() {
         if (this.map !is null) this.map.removeUnit(this);
         if (this.faction !is null) this.faction.removeUnit(this);
         if (this.currentTile !is null) this.currentTile.occupant 
= null;
         destroy(this);
     }
```
The `die` function without object destruction:
```
void die() {
         if (this.map !is null) this.map.removeUnit(this);
         if (this.faction !is null) this.faction.removeUnit(this);
         if (this.currentTile !is null) this.currentTile.occupant 
= null;
     }
```

They're the same, except that the latter doesn't call `destroy`. 
The other 3 lines are required to remove references to the 
object, effectively removing it from the game.

With the latter version, I suppose that the garbage collector 
should eventually clean up the "dead" unit object now that there 
are no references to it. However, I can see this leading to bugs 
if there was another reference to the unit which I forgot to 
remove. One benefit I see in destroying the object when it's no 
longer needed is that an error will happen if any remaining 
reference to the object gets accessed, rather than it leading to 
unexpected behaviour.

However I've thought about having it destroy the unit object at 
the end of the turn rather than immediately. Another option, if I 
don't want this benefit for debugging but still want fewer 
deallocations in the end result, would be to set that last line 
to `version (debug) destroy (this)`.

Anyway, I made [a 
commit](https://github.com/LiamM32/Open_Emblem/commit/64109e556a09ecce73b1018a9e651744a5e8fcd9) a few days ago that solves the unittest error. I found that explicitly destroying every `Map` object at the end of each unittest that uses it resolved the error. Despite this resolving the error, I decided to also move those lines from the `Unit` destructor to the new `die` function. I currently have it call `destroy` on itself at the end of this new function for the reasons described, but I suppose this line can be removed if I want to.

> And here's the important point: the two *do not need to 
> coincide*. Here's a concrete example of what I mean. Suppose in 
> your game there's some in-game mechanic that's creating N 
> objects per M turns, and another mechanic that's destroying 
> some of these objects every L turns.  If you map these 
> creations/destructions with the object lifetime, you're looking 
> at a *lot* of memory allocations and deallocations throughout 
> the course of your game.  Memory allocations and deallocations 
> can be costly; this can become a problem if you're talking 
> about a large number of objects, or if they're being 
> created/destroyed very rapidly (e.g., they are fragments flying 
> out from explosions).  Since most of these objects are 
> identical in type, one way of optimizing the code is to 
> preallocate them: before starting your main loop, say you 
> allocate an array of say, 100 objects. Or 1000 or 10000, 
> however many you anticipate you'll need. These objects aren't 
> actually in the game world yet; you're merely reserving the 
> memory for them beforehand. Mark each of them with a 
> "live"-ness flag that indicates whether or not they're actually 
> in the game.  Then during your main loop, whenever you need to 
> create a new object of that type, don't allocate memory for it; 
> just find a non-live object in this array, set its fields to 
> the right values, and mark it "live".  Now it's a object in the 
> game.  When the object is destroyed in-game, don't deallocate 
> it; instead, just set its "live" flag to false.  Now you can 
> blast through hundreds and thousands of these objects without 
> incurring the cost of allocating and deallocating them every 
> time.  You also save on GC cost (there's nothing for the GC to 
> collect, so it doesn't need to run at all).

Well, I don't have any explosions or anything fancy like that. 
Either way, I think all this will run very very fast. The one 
idea I have that might change this is if I have the enemy AI look 
multiple turns ahead by cloning all game objects in order to 
simulate multiple future outcomes.

When you mention a "flag" to indicate whether they are "live", do 
you mean like a boolean member variable for the `Unit` object? 
Like `bool alive;`?

> My advice remains the same: just let the GC do its job. Don't 
> "optimize" prematurely.  Use a profiler to test your program 
> and identify its real bottlenecks before embarking on these 
> often needlessly complicated premature optimizations that may 
> turn out to be completely unnecessary.

Alright. I suppose that some of the optimization decisions I have 
made so far may have resulted in less readable code for little 
performance benefit. Now I'm trying to worry less about 
optimization. Everything has been very fast so far.

I haven't used a profiler yet, but I may like to try it.

> If you're conscious of performance, however, I'd say avoid 
> references where you can.  Since maps presumably will always 
> exist while the game is going on, why bother with references at 
> all?  Just use a struct to store the coordinates of the tile, 
> and look it up in the map.  Or if you need to distinguish 
> between tiles belonging to multiple simultaneous maps, then 
> store a reference to the parent map along with the coordinates, 
> then you'll be able to find the right Tile easily. This way 
> your maps can just store an array of Tile structs (single 
> allocation), instead of an array of Tile objects (M*N 
> allocations for an M×N map).

It's unlikely that I will have multiple maps running 
simultaneously, unless if I do the AI thing mentioned above. I've 
had a dilemma of passing around references to the tile object vs 
passing around the coordinates, as is mentioned in an earlier 
thread that I started. In what way do references slow down 
performance? Would passing around a pair of coordinates to 
functions be better?


More information about the Digitalmars-d-learn mailing list