phobos/std/experimental/allocator/building_blocks/segregator.d
Paul Backus e05ee968ac Region: don't use NullAllocator as a sentinel
Region will now always assume that its ParentAllocator is a real
allocator, and attempt to use its 'allocate' and 'deallocate' methods
accordingly.

The behavior previously provided by Region!(NullAllocator, ...) remains
available via BorrowedRegion!(...).

See issue 23090 for detailed rationale.
2022-09-19 16:31:20 -04:00

520 lines
18 KiB
D

// Written in the D programming language.
/**
Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/segregator.d)
*/
module std.experimental.allocator.building_blocks.segregator;
import std.experimental.allocator.common;
/**
Dispatches allocations (and deallocations) between two allocators ($(D
SmallAllocator) and `LargeAllocator`) depending on the size allocated, as
follows. All allocations smaller than or equal to `threshold` will be
dispatched to `SmallAllocator`. The others will go to `LargeAllocator`.
If both allocators are `shared`, the `Segregator` will also offer $(D
shared) methods.
*/
struct Segregator(size_t threshold, SmallAllocator, LargeAllocator)
{
import std.algorithm.comparison : min;
import std.traits : hasMember, ReturnType;
import std.typecons : Ternary;
static if (stateSize!SmallAllocator) private SmallAllocator _small;
else private alias _small = SmallAllocator.instance;
static if (stateSize!LargeAllocator) private LargeAllocator _large;
else private alias _large = LargeAllocator.instance;
version (StdDdoc)
{
/**
The alignment offered is the minimum of the two allocators' alignment.
*/
enum uint alignment;
/**
This method is defined only if at least one of the allocators defines
it. The good allocation size is obtained from `SmallAllocator` if $(D
s <= threshold), or `LargeAllocator` otherwise. (If one of the
allocators does not define `goodAllocSize`, the default
implementation in this module applies.)
*/
static size_t goodAllocSize(size_t s);
/**
The memory is obtained from `SmallAllocator` if $(D s <= threshold),
or `LargeAllocator` otherwise.
*/
void[] allocate(size_t);
/**
This method is defined if both allocators define it, and forwards to
`SmallAllocator` or `LargeAllocator` appropriately.
*/
void[] alignedAllocate(size_t, uint);
/**
This method is defined only if at least one of the allocators defines
it. If `SmallAllocator` defines `expand` and $(D b.length +
delta <= threshold), the call is forwarded to `SmallAllocator`. If $(D
LargeAllocator) defines `expand` and $(D b.length > threshold), the
call is forwarded to `LargeAllocator`. Otherwise, the call returns
`false`.
*/
bool expand(ref void[] b, size_t delta);
/**
This method is defined only if at least one of the allocators defines
it. If `SmallAllocator` defines `reallocate` and $(D b.length <=
threshold && s <= threshold), the call is forwarded to $(D
SmallAllocator). If `LargeAllocator` defines `expand` and $(D
b.length > threshold && s > threshold), the call is forwarded to $(D
LargeAllocator). Otherwise, the call returns `false`.
*/
bool reallocate(ref void[] b, size_t s);
/**
This method is defined only if at least one of the allocators defines
it, and work similarly to `reallocate`.
*/
bool alignedReallocate(ref void[] b, size_t s, uint a);
/**
This method is defined only if both allocators define it. The call is
forwarded to `SmallAllocator` if $(D b.length <= threshold), or $(D
LargeAllocator) otherwise.
*/
Ternary owns(void[] b);
/**
This function is defined only if both allocators define it, and forwards
appropriately depending on `b.length`.
*/
bool deallocate(void[] b);
/**
This function is defined only if both allocators define it, and calls
`deallocateAll` for them in turn.
*/
bool deallocateAll();
/**
This function is defined only if both allocators define it, and returns
the conjunction of `empty` calls for the two.
*/
Ternary empty();
}
/**
Composite allocators involving nested instantiations of `Segregator` make
it difficult to access individual sub-allocators stored within. $(D
allocatorForSize) simplifies the task by supplying the allocator nested
inside a `Segregator` that is responsible for a specific size `s`.
Example:
----
alias A = Segregator!(300,
Segregator!(200, A1, A2),
A3);
A a;
static assert(typeof(a.allocatorForSize!10) == A1);
static assert(typeof(a.allocatorForSize!250) == A2);
static assert(typeof(a.allocatorForSize!301) == A3);
----
*/
ref auto allocatorForSize(size_t s)()
{
static if (s <= threshold)
static if (is(SmallAllocator == Segregator!(Args), Args...))
return _small.allocatorForSize!s;
else return _small;
else
static if (is(LargeAllocator == Segregator!(Args), Args...))
return _large.allocatorForSize!s;
else return _large;
}
enum uint alignment = min(SmallAllocator.alignment,
LargeAllocator.alignment);
private template Impl()
{
size_t goodAllocSize(size_t s)
{
return s <= threshold
? _small.goodAllocSize(s)
: _large.goodAllocSize(s);
}
void[] allocate(size_t s)
{
return s <= threshold ? _small.allocate(s) : _large.allocate(s);
}
static if (hasMember!(SmallAllocator, "alignedAllocate")
&& hasMember!(LargeAllocator, "alignedAllocate"))
void[] alignedAllocate(size_t s, uint a)
{
return s <= threshold
? _small.alignedAllocate(s, a)
: _large.alignedAllocate(s, a);
}
static if (hasMember!(SmallAllocator, "expand")
|| hasMember!(LargeAllocator, "expand"))
bool expand(ref void[] b, size_t delta)
{
if (!delta) return true;
if (b.length + delta <= threshold)
{
// Old and new allocations handled by _small
static if (hasMember!(SmallAllocator, "expand"))
return _small.expand(b, delta);
else
return false;
}
if (b.length > threshold)
{
// Old and new allocations handled by _large
static if (hasMember!(LargeAllocator, "expand"))
return _large.expand(b, delta);
else
return false;
}
// Oops, cross-allocator transgression
return false;
}
static if (hasMember!(SmallAllocator, "reallocate")
|| hasMember!(LargeAllocator, "reallocate"))
bool reallocate(ref void[] b, size_t s)
{
static if (hasMember!(SmallAllocator, "reallocate"))
if (b.length <= threshold && s <= threshold)
{
// Old and new allocations handled by _small
return _small.reallocate(b, s);
}
static if (hasMember!(LargeAllocator, "reallocate"))
if (b.length > threshold && s > threshold)
{
// Old and new allocations handled by _large
return _large.reallocate(b, s);
}
// Cross-allocator transgression
return .reallocate(this, b, s);
}
static if (hasMember!(SmallAllocator, "alignedReallocate")
|| hasMember!(LargeAllocator, "alignedReallocate"))
bool alignedReallocate(ref void[] b, size_t s, uint a)
{
static if (hasMember!(SmallAllocator, "alignedReallocate"))
if (b.length <= threshold && s <= threshold)
{
// Old and new allocations handled by _small
return _small.alignedReallocate(b, s, a);
}
static if (hasMember!(LargeAllocator, "alignedReallocate"))
if (b.length > threshold && s > threshold)
{
// Old and new allocations handled by _large
return _large.alignedReallocate(b, s, a);
}
// Cross-allocator transgression
return .alignedReallocate(this, b, s, a);
}
static if (hasMember!(SmallAllocator, "allocateZeroed")
|| hasMember!(LargeAllocator, "allocateZeroed"))
package(std) void[] allocateZeroed()(size_t s)
{
if (s <= threshold)
{
static if (hasMember!(SmallAllocator, "allocateZeroed"))
return _small.allocateZeroed(s);
else
{
auto b = _small.allocate(s);
(() @trusted => (cast(ubyte[]) b)[] = 0)(); // OK even if b is null.
return b;
}
}
else
{
static if (hasMember!(LargeAllocator, "allocateZeroed"))
return _large.allocateZeroed(s);
else
{
auto b = _large.allocate(s);
(() @trusted => (cast(ubyte[]) b)[] = 0)(); // OK even if b is null.
return b;
}
}
}
static if (hasMember!(SmallAllocator, "owns")
&& hasMember!(LargeAllocator, "owns"))
Ternary owns(void[] b)
{
return Ternary(b.length <= threshold
? _small.owns(b) : _large.owns(b));
}
static if (hasMember!(SmallAllocator, "deallocate")
&& hasMember!(LargeAllocator, "deallocate"))
bool deallocate(void[] data)
{
return data.length <= threshold
? _small.deallocate(data)
: _large.deallocate(data);
}
static if (hasMember!(SmallAllocator, "deallocateAll")
&& hasMember!(LargeAllocator, "deallocateAll"))
bool deallocateAll()
{
// Use & insted of && to evaluate both
return _small.deallocateAll() & _large.deallocateAll();
}
static if (hasMember!(SmallAllocator, "empty")
&& hasMember!(LargeAllocator, "empty"))
Ternary empty()
{
return _small.empty & _large.empty;
}
static if (hasMember!(SmallAllocator, "resolveInternalPointer")
&& hasMember!(LargeAllocator, "resolveInternalPointer"))
Ternary resolveInternalPointer(const void* p, ref void[] result)
{
Ternary r = _small.resolveInternalPointer(p, result);
return r == Ternary.no ? _large.resolveInternalPointer(p, result) : r;
}
}
private enum sharedMethods =
!stateSize!SmallAllocator
&& !stateSize!LargeAllocator
&& is(typeof(SmallAllocator.instance) == shared)
&& is(typeof(LargeAllocator.instance) == shared);
static if (sharedMethods)
{
static shared Segregator instance;
shared { mixin Impl!(); }
}
else
{
static if (!stateSize!SmallAllocator && !stateSize!LargeAllocator)
__gshared Segregator instance;
mixin Impl!();
}
}
///
@system unittest
{
import std.experimental.allocator.building_blocks.free_list : FreeList;
import std.experimental.allocator.gc_allocator : GCAllocator;
import std.experimental.allocator.mallocator : Mallocator;
alias A =
Segregator!(
1024 * 4,
Segregator!(
128, FreeList!(Mallocator, 0, 128),
GCAllocator),
Segregator!(
1024 * 1024, Mallocator,
GCAllocator)
);
A a;
auto b = a.allocate(200);
assert(b.length == 200);
a.deallocate(b);
}
/**
A `Segregator` with more than three arguments expands to a composition of
elemental `Segregator`s, as illustrated by the following example:
----
alias A =
Segregator!(
n1, A1,
n2, A2,
n3, A3,
A4
);
----
With this definition, allocation requests for `n1` bytes or less are directed
to `A1`; requests between $(D n1 + 1) and `n2` bytes (inclusive) are
directed to `A2`; requests between $(D n2 + 1) and `n3` bytes (inclusive)
are directed to `A3`; and requests for more than `n3` bytes are directed
to `A4`. If some particular range should not be handled, `NullAllocator`
may be used appropriately.
*/
template Segregator(Args...)
if (Args.length > 3)
{
// Binary search
private enum cutPoint = ((Args.length - 2) / 4) * 2;
static if (cutPoint >= 2)
{
alias Segregator = .Segregator!(
Args[cutPoint],
.Segregator!(Args[0 .. cutPoint], Args[cutPoint + 1]),
.Segregator!(Args[cutPoint + 2 .. $])
);
}
else
{
// Favor small sizes
alias Segregator = .Segregator!(
Args[0],
Args[1],
.Segregator!(Args[2 .. $])
);
}
}
///
@system unittest
{
import std.experimental.allocator.building_blocks.free_list : FreeList;
import std.experimental.allocator.gc_allocator : GCAllocator;
import std.experimental.allocator.mallocator : Mallocator;
alias A =
Segregator!(
128, FreeList!(Mallocator, 0, 128),
1024 * 4, GCAllocator,
1024 * 1024, Mallocator,
GCAllocator
);
A a;
auto b = a.allocate(201);
assert(b.length == 201);
a.deallocate(b);
}
@system unittest
{
import std.experimental.allocator.gc_allocator : GCAllocator;
import std.experimental.allocator.building_blocks.kernighan_ritchie : KRRegion;
Segregator!(128, GCAllocator, KRRegion!GCAllocator) alloc;
assert((() nothrow @safe @nogc => alloc.goodAllocSize(1))()
== GCAllocator.instance.goodAllocSize(1));
// Note: we infer `shared` from GCAllocator.goodAllocSize so we need a
// shared object in order to be able to use the function
shared Segregator!(128, GCAllocator, GCAllocator) sharedAlloc;
assert((() nothrow @safe @nogc => sharedAlloc.goodAllocSize(1))()
== GCAllocator.instance.goodAllocSize(1));
}
@system unittest
{
import std.experimental.allocator.building_blocks.bitmapped_block : BitmappedBlock;
import std.typecons : Ternary;
alias A =
Segregator!(
128, BitmappedBlock!(4096),
BitmappedBlock!(4096)
);
A a = A(
BitmappedBlock!(4096)(new ubyte[4096 * 1024]),
BitmappedBlock!(4096)(new ubyte[4096 * 1024])
);
assert(a.empty == Ternary.yes);
auto b = a.allocate(42);
assert(b.length == 42);
assert(a.empty == Ternary.no);
assert(a.alignedReallocate(b, 256, 512));
assert(b.length == 256);
assert(a.alignedReallocate(b, 42, 512));
assert(b.length == 42);
assert((() pure nothrow @safe @nogc => a.owns(b))() == Ternary.yes);
assert((() pure nothrow @safe @nogc => a.owns(null))() == Ternary.no);
// Ensure deallocate inherits from parent allocators
assert((() nothrow @nogc => a.deallocate(b))());
assert(a.empty == Ternary.yes);
// Test that deallocateAll inherits from parents
auto c = a.allocate(42);
assert(c.length == 42);
assert((() pure nothrow @safe @nogc => a.expand(c, 58))());
assert(c.length == 100);
assert(a.empty == Ternary.no);
assert((() nothrow @nogc => a.deallocateAll())());
assert(a.empty == Ternary.yes);
}
@system unittest
{
import std.experimental.allocator.gc_allocator : GCAllocator;
import std.typecons : Ternary;
shared Segregator!(1024 * 4, GCAllocator, GCAllocator) a;
auto b = a.allocate(201);
assert(b.length == 201);
void[] p;
assert((() nothrow @safe @nogc => a.resolveInternalPointer(&b[0], p))() == Ternary.yes);
assert((() nothrow @safe @nogc => a.resolveInternalPointer(null, p))() == Ternary.no);
// Ensure deallocate inherits from parent allocators
assert((() nothrow @nogc => a.deallocate(b))());
}
@system unittest
{
import std.experimental.allocator.building_blocks.bitmapped_block : BitmappedBlockWithInternalPointers;
import std.typecons : Ternary;
alias A =
Segregator!(
10_240, BitmappedBlockWithInternalPointers!(4096),
BitmappedBlockWithInternalPointers!(4096)
);
A a = A(
BitmappedBlockWithInternalPointers!(4096)(new ubyte[4096 * 1024]),
BitmappedBlockWithInternalPointers!(4096)(new ubyte[4096 * 1024])
);
assert((() nothrow @safe @nogc => a.empty)() == Ternary.yes);
auto b = a.allocate(201);
assert(b.length == 201);
assert((() nothrow @safe @nogc => a.empty)() == Ternary.no);
assert((() nothrow @nogc => a.deallocate(b))());
}
// Test that reallocate infers from parent
@system unittest
{
import std.experimental.allocator.mallocator : Mallocator;
alias a = Segregator!(10_240, Mallocator, Mallocator).instance;
auto b = a.allocate(42);
assert(b.length == 42);
assert((() nothrow @nogc => a.reallocate(b, 100))());
assert(b.length == 100);
assert((() nothrow @nogc => a.deallocate(b))());
}
@system unittest
{
import std.experimental.allocator.building_blocks.region : BorrowedRegion;
import std.typecons : Ternary;
auto a = Segregator!(10_240, BorrowedRegion!(), BorrowedRegion!())(
BorrowedRegion!()(new ubyte[4096 * 1024]),
BorrowedRegion!()(new ubyte[4096 * 1024]));
assert((() nothrow @safe @nogc => a.empty)() == Ternary.yes);
auto b = a.alignedAllocate(42, 8);
assert(b.length == 42);
assert((() nothrow @nogc => a.alignedReallocate(b, 100, 8))());
assert(b.length == 100);
assert((() nothrow @safe @nogc => a.empty)() == Ternary.no);
assert((() nothrow @nogc => a.deallocate(b))());
}