stdx.allocator

High-level interface for allocators. Implements bundled allocation/creation and destruction/deallocation of data including structs and classes, and also array primitives related to allocation. This module is the entry point for both making use of allocators and for their documentation.

$(BOOKTABLE, $(TR $(TH Category) $(TH Functions)) $(TR $(TD Make) $(TD $(LREF make) $(LREF makeArray) $(LREF makeMultidimensionalArray) )) $(TR $(TD Dispose) $(TD $(LREF dispose) $(LREF disposeMultidimensionalArray) )) $(TR $(TD Modify) $(TD $(LREF expandArray) $(LREF shrinkArray) )) $(TR $(TD Global) $(TD $(LREF processAllocator) $(LREF theAllocator) )) $(TR $(TD Class interface) $(TD $(LREF allocatorObject) $(LREF CAllocatorImpl) $(LREF IAllocator) )) )

Synopsis:

1 // Allocate an int, initialize it with 42
2 int* p = theAllocator.make!int(42);
3 assert(*p == 42);
4 // Destroy and deallocate it
5 theAllocator.dispose(p);
6 
7 // Allocate using the global process allocator
8 p = processAllocator.make!int(100);
9 assert(*p == 100);
10 // Destroy and deallocate
11 processAllocator.dispose(p);
12 
13 // Create an array of 50 doubles initialized to -1.0
14 double[] arr = theAllocator.makeArray!double(50, -1.0);
15 // Append two zeros to it
16 theAllocator.expandArray(arr, 2, 0.0);
17 // On second thought, take that back
18 theAllocator.shrinkArray(arr, 2);
19 // Destroy and deallocate
20 theAllocator.dispose(arr);

Layered Structure

D's allocators have a layered structure in both implementation and documentation:

  1. A high-level, dynamically-typed layer (described further down in this module). It consists of an interface called IAllocator, which concret; allocators need to implement. The interface primitives themselves are oblivious to the type of the objects being allocated; they only deal in void[], by necessity of the interface being dynamic (as opposed to type-parameterized). Each thread has a current allocator it uses by default, which is a thread-local variable theAllocator of type IAllocator. The process has a global allocator called processAllocator, also of type IAllocator. When a new thread is created, processAllocator is copied into theAllocator. An application can change the objects to which these references point. By default, at application startup, processAllocator refers to an object that uses D's garbage collected heap. This layer also include high-level functions such as make and dispose that comfortably allocate/create and respectively destroy/deallocate objects. This layer is all needed for most casual uses of allocation primitives.
  2. A mid-level, statically-typed layer for assembling several allocators into one. It uses properties of the type of the objects being created to route allocation requests to possibly specialized allocators. This layer is relatively thin and implemented and documented in the std.experimental._allocator.typed module. It allows an interested user to e.g. use different allocators for arrays versus fixed-sized objects, to the end of better overall performance.
  3. A low-level collection of highly generic heap building blocks$(MDASH) Lego-like pieces that can be used to assemble application-specific allocators. The real allocation smarts are occurring at this level. This layer is of interest to advanced applications that want to configure their own allocators. A good illustration of typical uses of these building blocks is module std.experimental._allocator.showcase which defines a collection of frequently- used preassembled allocator objects. The implementation and documentation entry point is std.experimental._allocator.building_blocks. By design, the primitives of the static interface have the same signatures as the IAllocator primitives but are for the most part optional and driven by static introspection. The parameterized class CAllocatorImpl offers an immediate and useful means to package a static low-level allocator into an implementation of IAllocator.
  4. Core allocator objects that interface with D's garbage collected heap (std.experimental._allocator.gc_allocator), the C malloc family (std.experimental._allocator.mallocator), and the OS (std.experimental._allocator.mmap_allocator). Most custom allocators would ultimately obtain memory from one of these core allocators.

Idiomatic Use of stdx._allocator

As of this time, stdx._allocator is not integrated with D's built-in operators that allocate memory, such as new, array literals, or array concatenation operators. That means stdx._allocator is opt-in$(MDASH)applications need to make explicit use of it.

For casual creation and disposal of dynamically-allocated objects, use make, dispose, and the array-specific functions makeArray, expandArray, and shrinkArray. These use by default D's garbage collected heap, but open the application to better configuration options. These primitives work either with theAllocator but also with any allocator obtained by combining heap building blocks. For example:

1 void fun(size_t n)
2 {
3     // Use the current allocator
4     int[] a1 = theAllocator.makeArray!int(n);
5     scope(exit) theAllocator.dispose(a1);
6     ...
7 }

To experiment with alternative allocators, set theAllocator for the current thread. For example, consider an application that allocates many 8-byte objects. These are not well supported by the default allocator, so a free list _allocator would be recommended. To install one in main, the application would use:

1 void main()
2 {
3     import stdx.allocator.building_blocks.free_list
4         : FreeList;
5     theAllocator = allocatorObject(FreeList!8());
6     ...
7 }

Saving the IAllocator Reference For Later Use

As with any global resource, setting theAllocator and processAllocator should not be done often and casually. In particular, allocating memory with one allocator and deallocating with another causes undefined behavior. Typically, these variables are set during application initialization phase and last through the application.

To avoid this, long-lived objects that need to perform allocations, reallocations, and deallocations relatively often may want to store a reference to the allocator object they use throughout their lifetime. Then, instead of using theAllocator for internal allocation-related tasks, they'd use the internally held reference. For example, consider a user-defined hash table:

1 struct HashTable
2 {
3     private IAllocator _allocator;
4     this(size_t buckets, IAllocator allocator = theAllocator) {
5         this._allocator = allocator;
6         ...
7     }
8     // Getter and setter
9     IAllocator allocator() { return _allocator; }
10     void allocator(IAllocator a) { assert(empty); _allocator = a; }
11 }

Following initialization, the HashTable object would consistently use its _allocator object for acquiring memory. Furthermore, setting HashTable._allocator to point to a different allocator should be legal but only if the object is empty; otherwise, the object wouldn't be able to deallocate its existing state.

Using Allocators without IAllocator

Allocators assembled from the heap building blocks don't need to go through IAllocator to be usable. They have the same primitives as IAllocator and they work with make, makeArray, dispose etc. So it suffice to create allocator objects wherever fit and use them appropriately:

1 void fun(size_t n)
2 {
3     // Use a stack-installed allocator for up to 64KB
4     StackFront!65536 myAllocator;
5     int[] a2 = myAllocator.makeArray!int(n);
6     scope(exit) myAllocator.dispose(a2);
7     ...
8 }

In this case, myAllocator does not obey the IAllocator interface, but implements its primitives so it can work with makeArray by means of duck typing.

One important thing to note about this setup is that statically-typed assembled allocators are almost always faster than allocators that go through IAllocator. An important rule of thumb is: "assemble allocator first, adapt to IAllocator after". A good allocator implements intricate logic by means of template assembly, and gets wrapped with IAllocator (usually by means of allocatorObject) only once, at client level.

Modules

building_blocks
module stdx.allocator.building_blocks

Assembling Your Own Allocator

common
module stdx.allocator.common

Utility and ancillary artifacts of stdx.allocator. This module shouldn't be used directly; its functionality will be migrated into more appropriate parts of std.

gc_allocator
module stdx.allocator.gc_allocator
mallocator
module stdx.allocator.mallocator
mmap_allocator
module stdx.allocator.mmap_allocator
showcase
module stdx.allocator.showcase

Collection of typical and useful prebuilt allocators using the given components. User code would typically import this module and use its facilities, or import individual heap building blocks and assemble them.

typed
module stdx.allocator.typed

This module defines TypedAllocator, a statically-typed allocator that aggregates multiple untyped allocators and uses them depending on the static properties of the types allocated. For example, distinct allocators may be used for thread-local vs. thread-shared data, or for fixed-size data (struct, class objects) vs. resizable data (arrays).

Members

Classes

CAllocatorImpl
class CAllocatorImpl(Allocator, Flag!"indirect" indirect = No.indirect)

Implementation of IAllocator using Allocator. This adapts a statically-built allocator type to IAllocator that is directly usable by non-templated code.

CSharedAllocatorImpl
class CSharedAllocatorImpl(Allocator, Flag!"indirect" indirect = No.indirect)

Implementation of ISharedAllocator using Allocator. This adapts a statically-built, shareable across threads, allocator type to ISharedAllocator that is directly usable by non-templated code.

Functions

allocatorObject
CAllocatorImpl!A allocatorObject(auto ref A a)
CAllocatorImpl!(A, Yes.indirect) allocatorObject(A* pa)

Returns a dynamically-typed CAllocator built around a given statically- typed allocator a of type A. Passing a pointer to the allocator creates a dynamic allocator around the allocator pointed to by the pointer, without attempting to copy or move it. Passing the allocator by value or reference behaves as follows.

dispose
void dispose(auto ref A alloc, auto ref T* p)
void dispose(auto ref A alloc, auto ref T p)
void dispose(auto ref A alloc, auto ref T[] array)

Destroys and then deallocates (using alloc) the object pointed to by a pointer, the class object referred to by a class or interface reference, or an entire array. It is assumed the respective entities had been allocated with the same allocator.

disposeMultidimensionalArray
void disposeMultidimensionalArray(auto ref Allocator alloc, auto ref T[] array)

Destroys and then deallocates a multidimensional array, assuming it was created with makeMultidimensionalArray and the same allocator was used.

expandArray
bool expandArray(auto ref Allocator alloc, ref T[] array, size_t delta)
bool expandArray(auto ref Allocator alloc, ref T[] array, size_t delta, auto ref T init)
bool expandArray(auto ref Allocator alloc, ref T[] array, R range)

Grows array by appending delta more elements. The needed memory is allocated using alloc. The extra elements added are either default- initialized, filled with copies of init, or initialized with values fetched from range.

make
auto make(auto ref Allocator alloc, auto ref A args)

Dynamically allocates (using alloc) and then creates in the memory allocated an object of type T, using args (if any) for its initialization. Initialization occurs in the memory allocated and is otherwise semantically the same as T(args). (Note that using alloc.make!(T[]) creates a pointer to an (empty) array of Ts, not an array. To use an allocator to allocate and initialize an array, use alloc.makeArray!T described below.)

makeArray
T[] makeArray(auto ref Allocator alloc, size_t length)
T[] makeArray(auto ref Allocator alloc, size_t length, auto ref T init)
Unqual!(ElementEncodingType!R)[] makeArray(auto ref Allocator alloc, R range)
T[] makeArray(auto ref Allocator alloc, R range)

Create an array of T with length elements using alloc. The array is either default-initialized, filled with copies of init, or initialized with values fetched from range.

makeMultidimensionalArray
auto makeMultidimensionalArray(auto ref Allocator alloc, size_t[N] lengths...)

Allocates a multidimensional array of elements of type T.

sharedAllocatorObject
shared(CSharedAllocatorImpl!A) sharedAllocatorObject(auto ref A a)
shared(CSharedAllocatorImpl!(A, Yes.indirect)) sharedAllocatorObject(A* pa)

Returns a dynamically-typed CSharedAllocator built around a given statically- typed allocator a of type A. Passing a pointer to the allocator creates a dynamic allocator around the allocator pointed to by the pointer, without attempting to copy or move it. Passing the allocator by value or reference behaves as follows.

shrinkArray
bool shrinkArray(auto ref Allocator alloc, ref T[] array, size_t delta)

Shrinks an array by delta elements.

Interfaces

IAllocator
interface IAllocator

Dynamic allocator interface. Code that defines allocators ultimately implements this interface. This should be used wherever a uniform type is required for encapsulating various allocator implementations.

ISharedAllocator
interface ISharedAllocator

Dynamic shared allocator interface. Code that defines allocators shareable across threads ultimately implements this interface. This should be used wherever a uniform type is required for encapsulating various allocator implementations.

Properties

processAllocator
shared(ISharedAllocator) processAllocator [@property getter]
void processAllocator [@property getter]

Gets/sets the allocator for the current process. This allocator must be used for allocating memory shared across threads. Objects created using this allocator can be cast to shared.

theAllocator
IAllocator theAllocator [@property getter]
void theAllocator [@property getter]

Gets/sets the allocator for the current thread. This is the default allocator that should be used for allocating thread-local memory. For allocating memory to be shared across threads, use processAllocator (below). By default, theAllocator ultimately fetches memory from processAllocator, which in turn uses the garbage collected heap.

Meta