std.allocator needs your help

Jacob Carlborg doob at me.com
Mon Sep 23 00:31:45 PDT 2013


On 2013-09-23 01:49, Andrei Alexandrescu wrote:

> 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.

I agree with Manu here. I thought the whole point was to come up with a 
design and API how the allocators are supposed to be used. How they 
integrate with user code.

Here's a quick idea:

I can think of at least four different usage patterns how an allocator 
can be set. Hopefully this will be backwards compatible as well.

1. Globally - The same allocator is used in the whole application by all 
threads

2. Thread local - The allocator is set per thread. Different threads can 
used different allocators

3. Function local - This is the most intrusive. Sets the allocator for a 
given function call

4. Block local - The allocator is used during a given block

This will require some interaction with the runtime and library and 
probably use OOP. We define two module variables in some module in 
druntime, say rt.allocator:

module rt.allocator:

shared Allocator globalAllocator;
Allocator allocator;

shared this ()
{
     globalAllocator = GCAllocator.allocator;
}

The global allocator is, by default, a GC allocator (the GC we're 
currently using). For each thread we set the thread local allocator to 
be the same as the global allocator:

allocator = globalAllocator;

By default all code will use "new" to allocate memory. druntime will 
have three functions which the "new" keyword is lowered to. They could 
look something like this:

extern (C) ubyte[] _d_tl_new (size_t size)
{
     return _d_new(size, rt.allocator.allocator);
}

extern (C) ubyte[] _d_global_new (size_t size)
{
     return _d_new(size, rt.allocator.globalAllocator);
}

extern (C) ubyte[] _d_new (size_t size, Allocator allocator)
{
     if (memory = allocator.allocate(size))
         return memory;

     onOutOfMemoryError();
}

By default "new" is lowered to a call to "_d_tl_new", which will 
allocate using the thread local allocator, which is by default the same 
as the global allocator, that is, the GC. In this way we maintain the 
current method of allocating memory.

When using "new shared", it's lowered to a function call to 
"_d_global_new", which uses the global allocator.

For block local allocator an ideal syntax would be:

allocator (GCAllocator.allocator)
{
     // explicitly use the GC allocator within this block.
}

If that's not possibly we can define a function look like this:

useAllocator (alias allocator, alias block) ()
{
     auto currentAllocator = core.allocator.allocator;
     scope (exit)
         core.allocator.allocator = currentAllocator;

     block();
}

Which is used, something like this:

useAllocator!(GCAllocator.allocator, {
     // explicitly use the GC allocator within this block.
});

Or alternately, using a struct:

struct UseAllocator
{
     private Allocator currentAlloctor;

     this (Allocator allocator)
     {
         currentAlloctor = core.allocator.allocator;
     }

     ~this ()
     {
         rt.allocator.allocator = currentAlloctor;
     }
}

UseAllocator useAllocator (Allocator allocator)
{
     return UseAllocator(allocator);
}


{
     auto _ = useAllocator(GCAllocator.allocator);
     // explicitly use the GC allocator within this block.
}

Of curse it's also possible to explicitly set the thread local or global 
allocator for a more fine grained control. The above functions are just 
for convenience and to make sure the allocator is restored.

Function local allocator would just be a function taking an allocator as 
a parameter, example:

void process (Allocator) (int a, Allocator allocator = 
core.allocator.allocator)
{
     auto _ = useAllocator(allocator);
     // use the given allocator throughout the rest of this function scope
}

Some bikeshedding:

> 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;
> }

I would put the asserts in an "in" block.

> * "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).

You have to come up with a better name than "it". If it's some kind of 
singleton instance then the standard name is usually "instance". Another 
suggested would be "allocator".

-- 
/Jacob Carlborg


More information about the Digitalmars-d mailing list