std.allocator needs your help

Andrei Alexandrescu SeeWebsiteForEmail at erdani.org
Sun Sep 22 16:49:57 PDT 2013


Hello,


I am making good progress on the design of std.allocator, and I am 
optimistic about the way it turns out. D's introspection capabilities 
really shine through, and in places the design does feel really 
archetypal - e.g. "this is the essence of a freelist allocator". It's a 
very good feeling. The overall inspiration comes from Berger's 
HeapLayers, but D's introspection takes that pattern to a whole new level.

Several details are still in flux, but at the top level it seems most 
natural to divide things in two tiers:

1. Typed allocator, i.e. every request for allocation comes with the 
exact type requested;

2. Untyped allocator - traffics exclusively in ubyte[].

Initially I thought it's impossible to separate the two, but it turns 
out there are profound generic realities about untyped allocators that 
make them the perfect foundation for typed allocators, without 
considerable abstraction costs.

I need to prove that before I'm sure, because so far I only have an 
archetype of untyped allocators, with typed allocators to follow. 
There's always the possibility that some hitch shows itself only when 
actually trying to implement various typed allocators.

Anyhow, here's the general interface of an untyped allocator. In fact 
I'll showcase the simplest allocator - the NullAllocator, which has zero 
memory to offer. (In spite of its do-absolutely-nothing stance, the 
NullAllocator is surprisingly useful as a "terminator" for certain 
layered allocators.) Anyhow, here it is (explanations follow):

struct NullAllocator
{
     enum alignment = real.alignof;
     enum size_t available = 0;
     ubyte[] allocate(size_t s) shared { return null; }
     bool expand(ref ubyte[] b, size_t minDelta, size_t maxDelta) shared
     { assert(b is null); return false; }
     bool reallocate(ref ubyte[] b, size_t) shared
     { assert(b is null); return false; }
     void deallocate(ubyte[] b) shared { assert(b is null); }
     void collect() shared { }
     void deallocateAll() shared { }
     static shared NullAllocator it;
}

Primitives:

* alignment is a compile-time constant yielding the alignment of 
allocated data. Many allocators offer the maximum alignment of the 
platform (for which I used real.alignof above). This constant is 
required for all allocators.

* available is a property that returns how many total (not necessarily 
contiguous) bytes are available for allocation. The NullAllocator knows 
statically it has 0 bytes available so it implements it as an enum. 
Generally allocators will implement it as a @property. This property is 
optional.

* allocate(s) returns a ubyte[] with length at least s, or null. (It 
does not throw on out-of-memory because that would hurt composability; 
it turns out many elemental allocators do naturally run out of memory.) 
This method is required for all allocators. In most allocators this 
method should be @safe.

* expand(b, minDelta, maxDelta) grows b's length by at least minDelta 
(and on a best-effort basis by at least maxDelta) and returns true, or 
does nothing and returns false. In most allocators this should be @safe. 
(One key insight is that expand() can be made @safe whereas shrink() or 
realloc() are most often not; such mini-epiphanies are very exciting 
because they put the design on a beam guide with few principles and many 
consequences.) The precondition is that b is null or has been previously 
returned by allocate(). This method is optional.

* reallocate(b, s) reallocates (like C's realloc) b to at least s bytes 
and returns true, or does nothing and returns false. It is possible that 
the memory is moved. This method is optional, and usually unsafe. (This 
was mostly introduced to accommodate C's realloc() within the allocator 
design.)

* deallocate(b) deallocates the memory allocated for b. b must have been 
previously allocated by the same allocator. This method is usually 
unsafe (but there are notable implementations that may offer safety, 
such as unbounded freelists.) This method is optional.

* deallocateAll() deallocates in one shot all memory previously 
allocated by this allocator. This method is optional, and when present 
is almost always unsafe (I see no meaningful @safe implementation.) 
Region allocators are notable examples of allocators that define this 
method.

* collect() frees unused memory that had been allocated with this 
allocator. This optional method is implemented by tracing collectors and 
is usually @safe.

* "it" is an interesting artifact. Allocators may or may not hold 
per-instance state. Those that don't are required to define a global 
shared or thread-local singleton called "it" that will be used for all 
calls related to allocation. Of course, it is preferred for maximum 
flexibility that "it" is shared so as to clarify that the allocator is 
safe to use among different threads. In the case of the NullAllocator, 
this is obviously true, but non-trivial examples are the malloc-based 
allocator and the global garbage collector, both of which implement 
thread-safe allocation (at great effort no less).

There are quite a few more things to define more precisely, but this 
part of the design has become quite stable. To validate the design, I've 
defined some simple allocators (Mallocator, GCAllocator, Freelist, 
StackRegion, Region etc) but not the more involved ones, such as 
coalescing heaps, buddy system etc.

If you have an interest in memory allocators and would like to design 
(or have around) an allocator that fits the API above, please post it. 
For layering purposes, don't call malloc() or sbrk() for getting large 
blocks of memory from the system; instead, use another Allocator as your 
parent allocator in a pattern as follows:

struct MyAllocator(Allocator)
{
     static if (is(typeof(Allocator.it))) alias Allocator.it _parent;
     else Allocator _parent;
     ... use _parent for getting memory ...
}

This allows MyAllocator to use stateful and stateless allocators 
transparently by just referring to _parent. Of course, your allocator 
may take other generic parameters in addition to Allocator, or take none 
at all if it's a "root" allocator (sbrk() or Mallocator would be examples).

Destroy!


Andrei


More information about the Digitalmars-d mailing list