Alternative Memory Management in D

Timur Gafarov
4 min readDec 11, 2018

--

D is a garbage collected language, and unfortunately this fact hinders its adoption by majority of game developers. Indeed, GC is not a helper for us. Writing effective and predictable real-time applications require full control over dynamic memory allocation and freeing. Games usually preallocate all the data in advance, and their memory consumption is highly deterministic. On top of that, game engines have to manage GPU data explicitly, so they can’t be easily combined with language-level GC without some tricky glue code. Most GC-centric languages either don’t allow manual memory management or make it frustratingly hard, which makes them a bad choice for writing a game engine. That’s why, aside from performance considerations, majority of game engines are written in C++.

Just to clarify: I’m okay with GC in D. While I’m fully for programmer control, I also believe that code complexity matters. GC is not evil, and D should not be blamed for having one. There are many cases where precise memory control can (and should) be sacrificed for programmer productivity. Games, however, are not such case, but this doesn’t mean D is not suitable for them.
In this article I will show how I do memory management in Dagon, proving that it’s perfectly possible to live without GC and feel comfortable. I’m not claiming that my methods are universal, but they do the job for me and probably can be useful for you too.

Reaching C++ level of abstraction

D is not Java or C#. Being a system language, it lets you do anything that you can do with C or C++. You can allocate memory by any available means, including GC, C-style malloc, external library, or a custom allocator. Thanks to powerful metaprogramming capabilities, D allows to hide an ugly allocation mechanism under a simple API. One such API is implemented in dlib — it provides New and Delete functions that wrap malloc and free. They are similar to new and delete operators in C++ — they allocate and free arrays, classes and structs, properly initializing them and calling constructors when necessary:

import dlib.core.memory;void main()
{
int[] array = New!(int[])(100);
Delete(array);

MyClass obj = New!MyClass(1, 2, 3);
Delete(obj);
}

Allocators

New is not just a syntactic sugar. Under the hood it is powered by abstract allocators from dlib.memory. Allocators are objects that implement a simple memory consumption interface (see dlib.memory.allocator). You can create your own allocator and replace dlib’s default one with it. Because allocator can allocate itself, it is rather simple. There are several alternative allocators in dlib, including GC-based one:

import dlib.core.memory;
import dlib.memory.gcallocator;
void main()
{
Allocator defaultAllocator = globalAllocator;
globalAllocator = GCallocator.instance;
// ‘New’ will use GCallocator

globalAllocator = defaultAllocator;
// ‘New’ will again use default allocator based on malloc
}

Ownership

Plain New and Delete may be enough for many simple cases, but if you’re writing an application with lots of objects things can quickly become complicated. The memory may end up leaking if you forget to delete some object, and bugs like that are not the easiest to fix. Garbage collection is there to solve this problem, but it brings plenty of other issues, so dlib provides an alternative — object ownership.

Core concept of ownership is that any object may ‘belong’ to other object. When the owner object is deleted, all its belonging objects are also automatically deleted.

import dlib.core.memory;
import dlib.core.ownership;
class MyClass: Owner
{
this(Owner owner = null)
{
super(owner);
}
}
void main()
{
MyClass c1 = New!MyClass();
MyClass c2 = New!MyClass(c1);
Delete(c1); // c2 is deleted automatically
}

This, of course, imposes some restrictions on object’s lifetime, but the idea of ownership can be applied to many use cases, GUI and game engines being two common examples. If you can think of your data model as a tree, ownership is a perfect memory management policy for it.

If you want to release non-owned entities (like arrays) or external resources (such as library or VRAM data) allocated by your object, do it in object’s destructor:

class MyClass: Owner
{
//...
~this()
{
Delete(myArray);
free(myExternalData);
}
}

Conclusion

D’s native memory management with GC
Pros: easiest to use, part of the language syntax.
Cons: unpredictable, requires additional logic to correctly handle external resources, still can cause memory leaks if used carelessly.
Summary: best suited for applications that constantly allocate short-living objects, or for utilities that perform one task and then exit. Not suitable for game engines that work heavily with VRAM data, which cannot be managed with GC.

Pure New/Delete
Pros: predictable, gives full control over memory management.
Cons: requires too much discipline, inconvenient to use in big projects, often causes memory leaks and double free errors
Summary: best suited for relatively small apps and games with simple engine architecture. In large projects turns code into unmaintainable mess.

New/Delete with ownership
Pros: predictable, eliminates memory leaks, requires little or no programmer attention, makes code clean and readable even in large projects.
Cons: constraints objects lifetime, works only with classes, can be an overkill for small applications.
Summary: best suited for complex games and GUI applications. Useless if you don’t write in object-oriented style.

--

--