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?