Allocating Memory

Get acquainted with the memory allocation methods in D.

We'll cover the following

Memory allocation methods

System languages allow programmers to specify the memory areas where objects should live. Such memory areas are commonly called buffers.

There are several methods of allocating memory. The simplest method is using a fixed-length array:

ubyte[100] buffer;    // A memory area of 100 bytes

buffer is ready to be used as a 100-byte memory area. Instead of ubyte, it is also possible to define such buffers as arrays of void, without any association to any type. Since void cannot be assigned any value, it cannot have the .init value either. Such arrays must be initialized by the special syntax =void:

void[100] buffer = void; // A memory area of 100 bytes

We will use only GC.calloc from the core.memory module to reserve memory. That module has many other features that are useful in various situations.

Additionally, the memory allocation functions of the C standard library are available in the core.stdc.stdlib module. GC.calloc allocates a memory area of the specified size pre-filled with all 0 values, and returns the beginning address of the allocated area:

import core.memory; 
// ...
    void * buffer = GC.calloc(100);
        // A memory area of 100 zero bytes

Normally, the returned void* value is cast to a pointer of the proper type:

int * intBuffer = cast(int*)buffer;

However, the intermediate step is usually skipped, and the return value is cast directly:

int * intBuffer = cast(int*)GC.calloc(100);

Instead of arbitrary values like 100, the size of the memory area is usually calculated by multiplying the number of elements needed with the size of each element:

// Allocate room for 25 ints
int * intBuffer = cast(int*)GC.calloc(int.sizeof * 25);

There is an important difference for classes: The size of a class variable and the size of a class object are not the same. .sizeof is the size of a class variable and is always the same value: 8 on 64-bit systems and 4 on 32-bit systems. The size of a class object must be obtained by __traits(classInstanceSize):

// Allocate room for 10 MyClass objects
MyClass * buffer = 
    cast(MyClass*)GC.calloc(
          __traits(classInstanceSize, MyClass) * 10);

When there is not enough memory in the system for the requested size, then a core.exception.OutOfMemoryError exception is thrown:

void * buffer = GC.calloc(10_000_000_000);

The output on a system that does not have that much free space is:

core.exception.OutOfMemoryError

The memory areas that are allocated from the GC can be returned back to it using GC.free:

GC.free(buffer);

However, calling free() does not necessarily execute the destructors of the variables that live on that memory block. The destructors may be executed explicitly by calling destroy() for each variable. Note that various internal mechanisms are used to call finalizers on class and struct variables during GC collection or freeing. The best way to ensure that these are called is to use the new operator when allocating variables. In this case, GC.free will call the destructors.

Sometimes, the program may determine that a previously allocated memory area is all used up and does not have room for more data. It is possible to extend a previously allocated memory area by GC.realloc. realloc() takes the previously allocated memory pointer and the newly requested size, and it returns a new area:

    void * oldBuffer = GC.calloc(100); 

// ...
    
    void * newBuffer = GC.realloc(oldBuffer, 200);

realloc() tries to be efficient by not actually allocating new memory unless it is really necessary:

  • If the memory area following the old area is not in use for any other purpose and is large enough to satisfy the new request, realloc() adds that part of the memory to the old area, extending the buffer in-place.

  • If the memory area following the old area is already in use or is not large enough, then realloc() allocates a new larger memory area and copies the contents of the old area to the new one.

  • It is possible to pass null as oldBuffer, in which case realloc() simply allocates new memory.

  • It is possible to pass a size less than the previous one, in which case the remaining part of the old memory is returned back to the GC.

  • It is possible to pass 0 as the new size, in which case realloc() simply frees the memory.

GC.realloc is adapted from the C standard library function realloc(). For having such a complicated behavior, realloc() is considered to have a badly designed function interface. A potentially surprising aspect of GC.realloc is that even if the original memory has been allocated with GC.calloc, the extended part is never cleared. Therefore, when it is important that the memory is zero-initialized, a function like reallocCleared() below would be useful. You will see the meaning of blockAttributes later:

Get hands-on with 1000+ tech skills courses.