Module std.experimental.allocator
High-level interface for allocators. Implements bundled allocation/creation
and destruction/deallocation of data including struct
s and class
es,
and also array primitives related to allocation. This module is the entry point
for both making use of allocators and for their documentation.
Category | Functions |
---|---|
Make | make
makeArray
makeMultidimensionalArray
|
Dispose | dispose
disposeMultidimensionalArray
|
Modify | expandArray
shrinkArray
|
Global | processAllocator
theAllocator
|
Class interface | allocatorObject
CAllocatorImpl
IAllocator
|
Synopsis
// Allocate an int, initialize it with 42
int* p = theAllocator .make!int(42);
assert(*p == 42);
// Destroy and deallocate it
theAllocator .dispose(p);
// Allocate using the global process allocator
p = processAllocator .make!int(100);
assert(*p == 100);
// Destroy and deallocate
processAllocator .dispose(p);
// Create an array of 50 doubles initialized to -1.0
double[] arr = theAllocator .makeArray!double(50, -1.0);
// Append two zeros to it
theAllocator .expandArray(arr, 2, 0.0);
// On second thought, take that back
theAllocator .shrinkArray(arr, 2);
// Destroy and deallocate
theAllocator .dispose(arr);
Layered Structure
D's allocators have a layered structure in both implementation and documentation:
- A high-level, dynamically-typed layer (described further down in this
module). It consists of an interface called
IAllocator
, which concrete allocators need to implement. The interface primitives themselves are oblivious to the type of the objects being allocated; they only deal invoid[]
, 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 variabletheAllocator
of typeIAllocator
. The process has a global allocator calledprocessAllocator
, also of typeIAllocator
. When a new thread is created,processAllocator
is copied intotheAllocator
. 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 asmake
anddispose
that comfortably allocate/create and respectively destroy/deallocate objects. This layer is all needed for most casual uses of allocation primitives. - 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
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..experimental .allocator .typed - A low-level collection of highly generic heap building blocks
— 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 modulestd
which defines a collection of frequently- used preassembled allocator objects. The implementation and documentation entry point is.experimental .allocator .showcase std
. By design, the primitives of the static interface have the same signatures as the.experimental .allocator .building_blocks IAllocator
primitives but are for the most part optional and driven by static introspection. The parameterized classCAllocatorImpl
offers an immediate and useful means to package a static low-level allocator into an implementation ofIAllocator
. - Core allocator objects that interface with D's garbage collected heap
(
std
), the C.experimental .allocator .gc_allocator malloc
family (std
), and the OS (.experimental .allocator .mallocator std
). Most custom allocators would ultimately obtain memory from one of these core allocators..experimental .allocator .mmap_allocator
Idiomatic Use of std .experimental .allocator
As of this time, std
is not integrated with D's
built-in operators that allocate memory, such as new
, array literals, or
array concatenation operators. That means std
is
opt-in
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:
void fun(size_t n)
{
// Use the current allocator
int[] a1 = theAllocator .makeArray!int(n);
scope(exit) theAllocator .dispose(a1);
...
}
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:
void main()
{
import std .experimental .allocator .building_blocks .free_list
: FreeList;
theAllocator = allocatorObject(FreeList!8());
...
}
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:
struct HashTable
{
private IAllocator allocator;
this(size_t buckets, IAllocator allocator = theAllocator) {
this .allocator = allocator;
...
}
// Getter and setter
IAllocator allocator() { return allocator; }
void allocator(IAllocator a) { assert(empty); allocator = a; }
}
Following initialization, the HashTable
object would consistently use its
allocator
object for acquiring memory. Furthermore, setting
HashTable
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:
void fun(size_t n)
{
// Use a stack-installed allocator for up to 64KB
StackFront!65536 myAllocator;
int[] a2 = myAllocator .makeArray!int(n);
scope(exit) myAllocator .dispose(a2);
...
}
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.
Functions
Name | Description |
---|---|
allocatorObject(a)
|
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(alloc, p)
|
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(alloc, array)
|
Destroys and then deallocates a multidimensional array, assuming it was created with makeMultidimensionalArray and the same allocator was used. |
expandArray(alloc, array, delta)
|
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(alloc, 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 creates a pointer to an (empty) array
of T s, not an array. To use an allocator to allocate and initialize an
array, use alloc described below.)
|
makeArray(alloc, length)
|
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()
|
Allocates a multidimensional array of elements of type T. |
processAllocator()
|
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 .
|
sharedAllocatorObject(a)
|
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(alloc, array, delta)
|
Shrinks an array by delta elements.
|
theAllocator()
|
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.
|
Interfaces
Name | Description |
---|---|
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
|
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. |
Classes
Name | Description |
---|---|
CAllocatorImpl
|
Implementation of IAllocator using Allocator . This adapts a
statically-built allocator type to IAllocator that is directly usable by
non-templated code.
|
CSharedAllocatorImpl
|
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.
|
Structs
Name | Description |
---|---|
RCIAllocator
|
A reference counted struct that wraps the dynamic allocator interface. This should be used wherever a uniform type is required for encapsulating various allocator implementations. |
RCISharedAllocator
|
A reference counted struct that wraps the dynamic shared allocator interface. This should be used wherever a uniform type is required for encapsulating various allocator implementations. |