Thread overview
Unittests pass, and then an invalid memory operation happens after?
March 27

In my current game project, something strange has happened as of a recent commit. When running dub test, all the unittests appear to pass, but then after the last unittest has concluded an "Invalid memory operation" happens. Removing a few lines replaces this error with a segfault, but either way, these errors shouldn't be happening. Weirdly, the commit where these errors first appear did nothing but replace a single class-member function with a seemingly identical function through a mixin template.

The errors seem to be related to object deletion. The traceback output for the first error, and where GDB points to for the second error, is the destructor for my Unit class.

You see, every Unit object is associated with a particular Map object and Faction object, which it hold references to. Those objects in turn each have an array of Unit objects that they are associated with. In the Unit destructor, I have it call the removeUnit function in both the associated Map and Faction objects. The errors are happening in either the Unit destructor itself, or the removeUnit function that it calls. Until the commit that introduced these errors, the removeUnit function was written directly in the Map class in it's module, but this commit replaced it with the mixin template UnitArrayManagement, which Faction also uses.

Unit destructor:

    ~this() {
        this.alive = false;
        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;
    }
template UnitArrayManagement(alias Unit[] unitsArray) {
    bool removeUnit(Unit unit) {
        import std.algorithm.searching;
        writeln("Doing `Map.removeUnit`");
        Unit[] shiftedUnits = unitsArray.find(unit);
        ushort unitKey = cast(ushort)(unitsArray.length - shiftedUnits.length);
        debug {
            writeln("unitsArray: ");
            foreach (listedUnit; unitsArray) writeln(listedUnit.name~", ");
            writeln("shiftedUnits: ");
            foreach (listedUnit; shiftedUnits) writeln(listedUnit.name~", ");
        }
        if (shiftedUnits.length > 0) {
            debug writeln("shiftedUnits.length > 0");
            unitsArray[$-shiftedUnits.length] = null;
            for (ushort i=0; i<shiftedUnits.length-1; i++) {
                debug writeln("In loop. unitKey = ", unitKey, ", i = ", i);
                unitsArray[unitKey+i] = unitsArray[unitKey+i+1];
            }
            unitsArray.length--;
            return true;
        } else return false;
    }
}

The first error happens because I inserted some uses of writeln for debugging purposes in the new version of removeUnit (because I haven't figured out how to do the same thing with GDB), in which I try to get it to print the names of all the units in the array before deleting any of them. I suppose that it might get a Invalid memory operation when trying to access a member of a Unit object that no longer exists, but this shouldn't be happening. When that other Unit object got destroyed, the destructor should have called this same removeUnit function to remove it's reference from the array.

I read that the garbage collector sometimes but not always calls destructors on deletion, which sounds crazy to me. Is this a case of one unit being deleted without the destructor and then the next unit (of the same Map object) having the destructor called?

Are destructors normally called after a program is concluded?

The second error, which can be achieved by removing the instances of writeln in removeUnit (making it seemingly identical now to the version of this function previously defined in the Map class) is also strange. It seems to be a case of the Unit object calling a Map object that no longer exists. However, that's also strange, as the Map object is supposed to delete all it's associated units on destruction.

I wrote these destructors so that objects wouldn't have references floating around on their deletion, yet now I'm getting errors from the destructors.

So why are these things even happening after the unittests have been run? What else do I need to know about object destruction? What may be happening?

March 27
On Wed, Mar 27, 2024 at 09:43:48PM +0000, Liam McGillivray via Digitalmars-d-learn wrote: [...]
> ```
>     ~this() {
>         this.alive = false;
>         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;
>     }
> ```
[...]

What's the definition of this.map, this.faction, and this.currentTile?

If any of them are class objects, this would be the cause of your problem.  Basically, when the dtor runs, there is no guarantee that any referenced classed objects haven't already been collected by the GC. So if you try to access them, it will crash with an invalid memory access.

In general, it's a bad idea to do anything that relies on the dtor being run in a particular order, because the GC can collect dead objects in any order.  It's also illegal to perform any GC memory-related operations inside a dtor (like allocate memory, free memory, etc.) because the GC is not reentrant.

If you need deterministic clean up of your objects, you should do it before the last reference to the object is deleted.


T

-- 
He who does not appreciate the beauty of language is not worthy to bemoan its flaws.
March 28

On Wednesday, 27 March 2024 at 21:43:48 UTC, Liam McGillivray wrote:

>

In my current game project, something strange has happened as of a recent commit. When running dub test, all the unittests appear to pass, but then after the last unittest has concluded an "Invalid memory operation" happens. Removing a few lines replaces this error with a segfault, but either way, these errors shouldn't be happening. Weirdly, the commit where these errors first appear did nothing but replace a single class-member function with a seemingly identical function through a mixin template.

The errors seem to be related to object deletion. The traceback output for the first error, and where GDB points to for the second error, is the destructor for my Unit class.

You see, every Unit object is associated with a particular Map object and Faction object, which it hold references to. Those objects in turn each have an array of Unit objects that they are associated with. In the Unit destructor, I have it call the removeUnit function in both the associated Map and Faction objects. The errors are happening in either the Unit destructor itself, or the removeUnit function that it calls. Until the commit that introduced these errors, the removeUnit function was written directly in the Map class in it's module, but this commit replaced it with the mixin template UnitArrayManagement, which Faction also uses.

Unit destructor:

    ~this() {
        this.alive = false;
        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;
    }

The GC does not guarantee destructor order. So this code is not valid -- e.g. you can't count on map to be a valid object at this point.

In my estimation, the code is not correct in principle anyway -- if the map has a pointer to the unit, then neither will be collected without both being garbage, and so there is no need to do these calls (they are all going away presently).

The only thing you should be doing in a destructor is freeing non-GC resources.

>

I read that the garbage collector sometimes but not always calls destructors on deletion, which sounds crazy to me.

The GC is not guaranteed to delete memory or run destructors. In the current implementation, it will destroy everything at the end of the program that was allocated using the GC, but the language does not guarantee this.

>

The second error, which can be achieved by removing the instances of writeln in removeUnit (making it seemingly identical now to the version of this function previously defined in the Map class) is also strange. It seems to be a case of the Unit object calling a Map object that no longer exists. However, that's also strange, as the Map object is supposed to delete all it's associated units on destruction.

As mentioned, GCs do not work this way -- you do not need to worry about cascading removal of anything.

You should assume in the destructor that all references in the type that were pointing at GC blocks are now invalid (i.e. dangling pointers).

>

So why are these things even happening after the unittests have been run? What else do I need to know about object destruction? What may be happening?

The GC is cleaning up all allocated memory, in no particular order.

-Steve

March 28

On Wednesday, 27 March 2024 at 22:14:16 UTC, H. S. Teoh wrote:

>

What's the definition of this.map, this.faction, and this.currentTile?

As was said, and can be found on the linked repository, they are references to class objects.

On Thursday, 28 March 2024 at 01:47:27 UTC, Steven Schveighoffer wrote:

>

On Wednesday, 27 March 2024 at 21:43:48 UTC, Liam McGillivray wrote:

>

Unit destructor:

    ~this() {
        this.alive = false;
        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;
    }

The GC does not guarantee destructor order. So this code is not valid -- e.g. you can't count on map to be a valid object at this point.

Well, this was originally done so that when I explicitly call destroy on a Unit object (which happens in-game) it will delete all references to itself.

Do you think it's better for me to move this code to a die function that I can call instead, which will then call for it's own destruction after running those function calls?

>

The only thing you should be doing in a destructor is freeing non-GC resources.

>

I read that the garbage collector sometimes but not always calls destructors on deletion, which sounds crazy to me.

The GC is not guaranteed to delete memory or run destructors. In the current implementation, it will destroy everything at the end of the program that was allocated using the GC, but the language does not guarantee this.

What's strange is that even if I explicitly call destroy for the only two objects at the end of the last unittest, it still happens. I assumed that each unittest was like it's own program, but I suppose not. It's deleting objects from an earlier unittest, right?

I may be now starting to see why the use of a garbage collector is such a point of contention for D. Not being able to predict how the garbage collection process will happen seems like a major problem.

>

As mentioned, GCs do not work this way -- you do not need to worry about cascading removal of anything.

Wanting to avoid the GC pauses that I hear about, I was trying to optimize object deletion so that the GC doesn't have to look for every object individually. It sounds like what I'm hearing is that I should just leave everything to the GC. While I can do this without really hurting the performance of my program (for now), I don't like this.

I hope that solving the unpredictable destruction pattern is a priority for the developers of the language. This problem in my program wouldn't be happening if either all of the objects had their destructors called or none of them did.

Anyway, I suppose I'll have to experiment with either manually destroying every object at the end of every unittest, or just leaving more to the GC. Maybe I'll make a separate die function for the units, if you think it's a good idea.

Also, check your Github notifications. I have something for you.

March 27
On Thu, Mar 28, 2024 at 03:56:10AM +0000, Liam McGillivray via Digitalmars-d-learn wrote: [...]
> I may be now starting to see why the use of a garbage collector is such a point of contention for D. Not being able to predict how the garbage collection process will happen seems like a major problem.

If you want it to be predictable, simply:

	import core.memory;
	GC.disable();
	... // insert your code here
	if (timeToCleanup()) {
		GC.collect();	// now you know exactly when this happens
	}

Of course, you'll have to know exactly how timeToCleanup() should decide when it's time to collect.  Simple possibilities are once every N units of time, once every N iterations of some main loop, etc..  Or use a profiler to decide.


> > As mentioned, GCs do not work this way -- you do not need to worry about cascading removal of anything.
> 
> Wanting to avoid the GC pauses that I hear about, I was trying to optimize object deletion so that the GC doesn't have to look for every object individually. It sounds like what I'm hearing is that I should just leave everything to the GC. While I can do this without really hurting the performance of my program (for now), I don't like this.

The whole point of a GC is that you leave everything up to it to clean up.  If you want to manage your own memory, don't use the GC. D does not force you to use it; you can import core.stdc.stdlib and use malloc/free to your heart's content.


> I hope that solving the unpredictable destruction pattern is a priority for the developers of the language. This problem in my program wouldn't be happening if either *all* of the objects had their destructors called or *none* of them did.

Unpredictable order of collection is an inherent property of GCs. It's not going away.  If you don't like it, use malloc/free instead. (Or write your own memory management scheme.)


> Anyway, I suppose I'll have to experiment with either manually destroying every object at the end of every unittest, or just leaving more to the GC.  Maybe I'll make a separate `die` function for the units, if you think it's a good idea.
[...]

I think you're approaching this from a totally wrong angle. (Which I sympathize with, having come from a C/C++ background myself.)  The whole point of having a GC is that you *don't* worry about when an object is collected.  You just allocate whatever you need, and let the GC worry about cleaning up after you. The more you let the GC do its job, the better it will be.

Now of course there are situations where you need deterministic destruction, such as freeing up system resources as soon as they're no longer needed (file descriptors, OS shared memory segments allocations, etc.). For these you would manage the memory manually (e.g. with a struct that implements reference counting or whatever is appropriate).

As far as performance is concerned, a GC actually has higher throughput than manually freeing objects, because in a fragmented heap situation, freeing objects immediately when they go out of use incurs a lot of random access RAM roundtrip costs, whereas a GC that scans memory for references can amortize some of this cost to a single period of time.

Now somebody coming from C/C++ would immediately cringe at the thought that a major GC collection might strike at the least opportune time. For that, I'd say:

(1) don't fret about it until it actually becomes a problem. I.e., your program is slow and/or has bad response times, and the profiler is pointing to GC collections as the cause. Then you optimize appropriately with the usual practices for GC optimization: preallocate before your main loop, avoid frequent allocations of small objects (prefer to use structs rather than classes), reuse previous allocations instead of allocating new memory when you know that an existing object is no longer used.  In D, you can also selectively allocate certain troublesome objects with malloc/free instead (mixing both types of allocations is perfectly fine in D; we are not Java where you're forced to use the GC no matter what).

(2) Use D's GC control mechanisms to exercise some control over when collections happen. By default, collections ONLY ever get triggered if you try to allocate something and the heap has run out of memory.  Ergo, if you don't allocate anything, GC collections are guaranteed not to happen.  Use GC.disable and GC.collect to control when collections happen.  In one of my projects, I got a 40% performance boost by using GC.disable and using my own schedule of GC.collect, because the profiler revealed that collections were happening too frequently.  The exact details how what to do will depend on your project, of course, but my point is, there are plenty of tools at your disposal to exercise some degree of control.

Or if (1) and (2) are not enough for your particular case, you can
always resort to the nuclear option: slap @nogc on main() and use
malloc/free to your heart's content. IME, however, this is very, very
rarely called for.  Generally a combination of (1) and (2) has been more
than adequate to fix my GC issues.


T

-- 
Computerese Irregular Verb Conjugation: I have preferences.  You have biases.  He/She has prejudices. -- Gene Wirchenko
March 28

On Thursday, 28 March 2024 at 04:46:27 UTC, H. S. Teoh wrote:

>

The whole point of a GC is that you leave everything up to it to clean up. If you want to manage your own memory, don't use the GC. D does not force you to use it; you can import core.stdc.stdlib and use malloc/free to your heart's content.

Unpredictable order of collection is an inherent property of GCs. It's not going away. If you don't like it, use malloc/free instead. (Or write your own memory management scheme.)

I disagree with this attitude on how the GC should work. Having to jump immediately from leaving everything behind for the GC to fully manual memory allocation whenever the GC becomes a problem is a problem, which gives legitimacy to the common complaint of D being "garbage-collected". It would be much better if the garbage collector could be there as a backup for when it's needed, while allowing the programmer to write code for object destruction when they want to optimize.

> >

Anyway, I suppose I'll have to experiment with either manually destroying every object at the end of every unittest, or just leaving more to the GC. Maybe I'll make a separate die function for the units, if you think it's a good idea.

I think you're approaching this from a totally wrong angle. (Which I sympathize with, having come from a C/C++ background myself.) The whole point of having a GC is that you don't worry about when an object is collected. You just allocate whatever you need, and let the GC worry about cleaning up after you. The more you let the GC do its job, the better it will be.

Now you're giving me conflicting advice. I was told that my current destructor functions aren't acceptable with the garbage collector, and you specifically tell me to leave things to the GC. But then I suggest that I "leave more to the GC" and move everything from the Unit destructor to a specialized die function that can be called instead of destroy whenever they must be removed from the game, which as far as I can see is the only way to achieve the desired game functionality while following your and Steve's advice and not having dangling references. But in response to that, you tell me "I think you're approaching this from the wrong angle". And then right after that, you again tell me to "just let the GC worry about cleaning up after you"? Even if I didn't call destroy at all during my program, as far as I can see, I would still need the die function mentioned to remove a unit on death.

It would be nice if you can clarify your message here. Right now I'm confused. I see no way to take your advice without also doing the die function.

>

As far as performance is concerned, a GC actually has higher throughput than manually freeing objects, because in a fragmented heap situation, freeing objects immediately when they go out of use incurs a lot of random access RAM roundtrip costs, whereas a GC that scans memory for references can amortize some of this cost to a single period of time.

By "manually freeing objects", do you mean through destroy? If so that's actually quite disappointing, as D is often described as a "systems programming language", and I thought it would be fun to do these optimizations of object destruction, even if I have the garbage collector as a backup for anything missed. Or did you mean with malloc and free?

>

Now somebody coming from C/C++ would immediately cringe at the thought that a major GC collection might strike at the least opportune time. For that, I'd say:

(1) don't fret about it until it actually becomes a problem. I.e., your program is slow and/or has bad response times, and the profiler is pointing to GC collections as the cause. Then you optimize appropriately with the usual practices for GC optimization: preallocate before your main loop, avoid frequent allocations of small objects (prefer to use structs rather than classes), reuse previous allocations instead of allocating new memory when you know that an existing object is no longer used.

Well, I suppose that's fine for when the GC problem is specifically over slowness. I'm quite new to D, so I don't really know what it means to "preallocate before your main loop". Is this a combination of using static this constructors and malloc? I haven't used malloc yet. I have tried making static constructors, but every time I've tried them, they caused an error to happen immediately after the program is launched.

I've used C++, but I haven't gotten much done with it. It was PHP where I made the biggest leap in my programming skills. This game is the furthest I've gone at making a complex program from the main loop up.

I suppose I can turn the Tile object into a struct, which I suppose will mean replacing all it's references (outside the map's Tile[][] grid) with pointers. I have thought about this before, since tiles are fundamentally associated with one particular map, but I chose objects mostly so I can easily pass around references to them.

I've already been using structs for the stuff with a short lifespan.

>

(2) Use D's GC control mechanisms to exercise some control over when collections happen. By default, collections ONLY ever get triggered if you try to allocate something and the heap has run out of memory. Ergo, if you don't allocate anything, GC collections are guaranteed not to happen. Use GC.disable and GC.collect to control when collections happen. In one of my projects, I got a 40% performance boost by using GC.disable and using my own schedule of GC.collect, because the profiler revealed that collections were happening too frequently. The exact details how what to do will depend on your project, of course, but my point is, there are plenty of tools at your disposal to exercise some degree of control.

>

always resort to the nuclear option: slap @nogc on main() and

I want to ask about @nogc. Does it simply place restrictions on what I can do? Or does it change the meaning of certain lines? For example, does it mean that I can still create objects, but they will just keep piling up without being cleaned up?

March 28
On Thu, Mar 28, 2024 at 11:49:19PM +0000, Liam McGillivray via Digitalmars-d-learn wrote:
> On Thursday, 28 March 2024 at 04:46:27 UTC, H. S. Teoh wrote:
> > The whole point of a GC is that you leave everything up to it to clean up.  If you want to manage your own memory, don't use the GC. D does not force you to use it; you can import core.stdc.stdlib and use malloc/free to your heart's content.
> > 
> > Unpredictable order of collection is an inherent property of GCs. It's not going away.  If you don't like it, use malloc/free instead. (Or write your own memory management scheme.)
> 
> I disagree with this attitude on how the GC should work. Having to jump immediately from leaving everything behind for the GC to fully manual memory allocation whenever the GC becomes a problem is a problem, which gives legitimacy to the common complaint of D being "garbage-collected". It would be much better if the garbage collector could be there as a backup for when it's needed, while allowing the programmer to write code for object destruction when they want to optimize.

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.


> > > Anyway, I suppose I'll have to experiment with either manually destroying every object at the end of every unittest, or just leaving more to the GC. Maybe I'll make a separate `die` function for the units, if you think it's a good idea.
> > 
> > I think you're approaching this from a totally wrong angle. (Which I sympathize with, having come from a C/C++ background myself.)  The whole point of having a GC is that you *don't* worry about when an object is collected.  You just allocate whatever you need, and let the GC worry about cleaning up after you. The more you let the GC do its job, the better it will be.
> 
> Now you're giving me conflicting advice. I was told that my current destructor functions aren't acceptable with the garbage collector, and you specifically tell me to leave things to the GC. But then I suggest that I "leave more to the GC" and move everything from the Unit destructor to a specialized `die` function that can be called instead of `destroy` whenever they must be removed from the game, which as far as I can see is the only way to achieve the desired game functionality while following your and Steve's advice and not having dangling references. But in response to that, you tell me "I think you're approaching this from the wrong angle". And then right after that, you *again* tell me to "just let the GC worry about cleaning up after you"? Even if I didn't call `destroy` at all during my program, as far as I can see, I would still need the `die` function mentioned to remove a unit on death.

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.

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.

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).

Don't get confused by the "object" terminology; an in-game object is not necessarily the same thing as a class object in your program. In fact, sometimes it's advantageous to treat them as separate things.


[...]
> > As far as performance is concerned, a GC actually has higher throughput than manually freeing objects, because in a fragmented heap situation, freeing objects immediately when they go out of use incurs a lot of random access RAM roundtrip costs, whereas a GC that scans memory for references can amortize some of this cost to a single period of time.
> 
> By "manually freeing objects", do you mean through `destroy`? If so that's actually quite disappointing, as D is often described as a "systems programming language", and I thought it would be fun to do these optimizations of object destruction, even if I have the garbage collector as a backup for anything missed. Or did you mean with `malloc` and `free`?

By "manually freeing objects" I mean what you typically do when you're using malloc/free.

Note that in D, you *can* actually "manually manage" some objects this way by calling GC.free.  I don't recommend this, though. It's not how the GC is intended to be used, and it can lead to memory safety problems.  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.


[...]
> Well, I suppose that's fine for when the GC problem is specifically over slowness. I'm quite new to D, so I don't really know what it means to "preallocate before your main loop". Is this a combination of using `static this` constructors and `malloc`? I haven't used `malloc` yet. I have tried making static constructors, but every time I've tried them, they caused an error to happen immediately after the program is launched.

I gave an example of preallocation above.  It's not specific to GC or malloc/free; it's a general principle of reducing the number of allocations inside your main loop. If you can reserve beforehand the memory you know you'll need later, it will generally perform better than if you keep allocating on-the-fly.  Allocation and deallocation (or GC collection) always come with a cost; so you want to avoid doing this inside a hot loop, like your main game loop (assuming it's running every frame -- otherwise it's a non-issue and you shouldn't worry about it). Allocating N objects beforehand and then gradually using them as needed, is always better than starting with 0 objects and allocating each one individually inside your hot loop.


[...]
> I suppose I can turn the `Tile` object into a struct, which I suppose will mean replacing all it's references (outside the map's `Tile[][] grid`) with pointers. I have thought about this before, since tiles are fundamentally associated with one particular map, but I chose objects mostly so I can easily pass around references to them.

The correct design will depend on how you're using them, so I can't give you a specific recommendation here.

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).


> I've already been using structs for the stuff with a short lifespan.

Good.


[...]
> I want to ask about `@nogc`. Does it simply place restrictions on what I can do? Or does it change the meaning of certain lines? For example, does it mean that I can still create objects, but they will just keep piling up without being cleaned up?

@nogc does not change runtime behaviour. It's a static enforcement that prevents your code from doing anything that might trigger GC allocations at all. So using anything that will trigger a GC allocation, such as using `new` or appending to an array with `~`, will cause a compile error.  I don't recommend using it (a pretty substantial chunk of the language and stdlib will become unavailable to you), but if you absolutely, totally, 100% want to abstain from using the GC at all, @nogc is your ticket to ensure that you didn't accidentally do so. The compiler will enforce it at compile-time.


T

-- 
Music critic: "That's an imitation fugue!"
April 03

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.

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 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?

April 06
On Wed, Apr 03, 2024 at 09:57:00PM +0000, Liam McGillivray via Digitalmars-d-learn wrote:
> 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).

https://dlang.org/phobos/core_memory.html#.GC.free


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

GC.free is low-level. It does not invoke dtors.


[...]
> 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;`?

Yes.


> > 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.

Never make any optimization decisions without a profiler. I learned the hard way that more often than not, I'm wrong about where my program's bottleneck is, and that I spend far too much time and effort "optimizing" things that don't need to be optimized, while totally missing optimizations where it really matters.  Life is too short to be wasted on optimizing things that don't really matter.  When it comes to optimizations, always profile, profile, profile!


[...]
> 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?

It's not references themselves that slow things down; it's the likelihood that using reference types when you don't need to can lead to excessive GC allocations, which in turn causes longer GC pauses.  Well, excessive dereferencing can also reduce cache coherence, but if you're already at the level where this actually makes a difference, you don't my advice anymore. :-D

Generally, if a piece of data is transient and not expected to last very long (e.g., past the current frame), it probably should be a struct rather than a class.  There are exceptions, of course, but generally that's how I'd decide whether something should be a by-value type or a by-reference type.


T

-- 
"The number you have dialed is imaginary. Please rotate your phone 90 degrees and try again."
April 07

On Wednesday, 3 April 2024 at 21:57:00 UTC, Liam McGillivray wrote:

>

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.

A good example where you people will get fooled is to avoid passing structs as function arguments/return values because you're worried about copying:

https://forum.dlang.org/post/jifumskhdmxkimtayzva@forum.dlang.org

I'm guilty of having done that regularly until I learned how the compiler actually works and how little time certain operations take. One time I even changed clear string arguments to hard-to-remember single char values inside a loop.