diff --git a/phobos/sys/allocators/alternatives/allocatorlist.d b/phobos/sys/allocators/alternatives/allocatorlist.d new file mode 100644 index 00000000000..815817bca05 --- /dev/null +++ b/phobos/sys/allocators/alternatives/allocatorlist.d @@ -0,0 +1,306 @@ +/** +Provides a growable list of memory allocator instances. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.alternatives.allocatorlist; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + // guarantee tha each strategy has been initialized + alias ALRC = AllocatorList!(RCAllocator, (RCAllocator* poolAllocator) => poolAllocator); +} + +export: + +/** +A simple allocator list that relies on a given allocators (as provided by the factory function) to provide its own memory. + +Supports isOnlyOneAllocationOfSize method on the pool allocator to allow knowing if it can free a given allocator instance. + +Does not use `TypeInfo`, but will be forwarded on allocation. +*/ +struct AllocatorList(PoolAllocator, alias factory) +{ +export: + static assert(__traits(hasMember, TypeOfAllocator, "deallocateAll"), + "Allocator allocated by factory function must have deallocateAll method."); + static assert(__traits(hasMember, TypeOfAllocator, "owns"), + "Allocator allocated by factory function must have owns method."); + + /// Source for all memory, passed by pointer/ref to factory function + PoolAllocator poolAllocator; + + /// + enum NeedsLocking = true; + + private + { + Node* head; + } + +scope @safe @nogc nothrow: + ~this() + { + deallocateAll(); + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + +@trusted: + + /// + this(return scope ref AllocatorList other) + { + this.tupleof = other.tupleof; + other.head = null; + other = AllocatorList.init; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + Node* current = head; + + while (current !is null) + { + Node* next = current.next; + + void[] ret = current.allocator.allocate(size, ti); + + if (ret.length >= size) + { + return ret; + } + else if (ret.length > 0) + { + current.allocator.deallocate(ret); + } + + current = next; + } + + expand(size); + assert(head !is null); + + if ((current = head) !is null) + { + void[] ret = current.allocator.allocate(size, ti); + + if (ret.length >= size) + { + return ret; + } + else if (ret.length > 0) + { + current.allocator.deallocate(ret); + } + } + + return null; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + Node* current = head; + + while (current !is null) + { + Node* next = current.next; + + if (current.allocator.owns(array) == Ternary.yes) + return current.allocator.reallocate(array, newSize); + + current = next; + } + + return false; + } + + /// + bool deallocate(scope void[] array) + { + Node** parent = &head; + Node* current = head; + + while (current !is null) + { + Node* next = current.next; + + if (current.allocator.owns(array) == Ternary.yes) + { + bool got = current.allocator.deallocate(array); + + if (got) + { + static if (__traits(hasMember, PoolAllocator, "isOnlyOneAllocationOfSize")) + { + if (current.allocator.isOnlyOneAllocationOfSize(Node.sizeof)) + { + PoolAllocator temp = current.allocator; + *parent = current.next; + temp.deallocateAll(); + } + } + + return true; + } + else + return false; + } + + parent = ¤t.next; + current = next; + } + + return false; + } + + /// + Ternary owns(scope void[] array) + { + Node* current = head; + + while (current !is null) + { + Node* next = current.next; + + if (current.allocator.owns(array) == Ternary.yes) + return Ternary.yes; + + current = next; + } + + return Ternary.no; + } + + /// + bool deallocateAll() + { + Node* current = head; + + while (current !is null) + { + Node* next = current.next; + + auto currentAllocator = current.allocator; + currentAllocator.deallocateAll(); + + current = next; + } + + head = null; + return true; + } + + /// + bool empty() + { + return false; + } + +private @hidden: + import std.traits : isPointer, ParameterStorageClassTuple, ParameterStorageClass; + + static if (isPointer!PoolAllocator || (__traits(compiles, factory!PoolAllocator) + && ParameterStorageClassTuple!(factory!PoolAllocator)[0] == ParameterStorageClass.ref_)) + alias TypeOfAllocator = typeof(factory(poolAllocator)); + else static if (__traits(compiles, typeof(factory()))) + alias TypeOfAllocator = typeof(factory()); + else + alias TypeOfAllocator = typeof(factory({ + typeof(poolAllocator)* ret; + return ret; + }())); + + static struct Node + { + TypeOfAllocator allocator; + Node* next; + } + + void expand(size_t requesting = 0) + { + import std.traits : isPointer, ParameterStorageClass, ParameterStorageClassTuple; + import std.algorithm : moveEmplace; + + TypeOfAllocator current; + + static if (isPointer!PoolAllocator || (__traits(compiles, factory!PoolAllocator) + && ParameterStorageClassTuple!(factory!PoolAllocator)[0] == ParameterStorageClass + .ref_)) + current = factory(poolAllocator); + else static if (__traits(compiles, typeof(factory()))) + current = factory(); + else + current = factory(&poolAllocator); + + if (requesting > 0) + requesting += size_t.sizeof * 8; + + void[] got = current.allocate(Node.sizeof + requesting); + if (got is null) + return; + + if (requesting > 0) + { + current.deallocate(got); + got = current.allocate(Node.sizeof); + } + + Node* currentNode = cast(Node*) got.ptr; + + moveEmplace(current, currentNode.allocator); + currentNode.next = head; + + head = currentNode; + } +} + +/// +unittest +{ + import phobos.sys.allocators.mapping.malloc; + import phobos.sys.allocators.buffers.region; + + alias AL = AllocatorList!(Region!Mallocator, () => Region!Mallocator()); + + AL al; + assert(!al.isNull); + assert(!al.empty); + + al = AL(); + assert(!al.isNull); + assert(!al.empty); + + void[] got = al.allocate(1024); + assert(got !is null); + assert(got.length == 1024); + assert(al.owns(got) == Ternary.yes); + assert(al.owns(got[10 .. 20]) == Ternary.yes); + + AL al2 = al; + al = al2; + + bool success = al.reallocate(got, 2048); + assert(success); + assert(got.length == 2048); + + assert(al.owns(null) == Ternary.no); + assert(al.owns(got) == Ternary.yes); + assert(al.owns(got[10 .. 20]) == Ternary.yes); + + success = al.deallocate(got); + assert(success); +} diff --git a/phobos/sys/allocators/alternatives/bucketizer.d b/phobos/sys/allocators/alternatives/bucketizer.d new file mode 100644 index 00000000000..377ddd7cbb0 --- /dev/null +++ b/phobos/sys/allocators/alternatives/bucketizer.d @@ -0,0 +1,153 @@ +/** +Groups similar sized allocations together into buckets. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.alternatives.bucketizer; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + alias BRC = Bucketizer!(RCAllocator, 0, 10, 1); +} + +export: + +/** +Uses buckets to segregate memory. + +Does not use `TypeInfo`, but will be forwarded on allocation. +*/ +struct Bucketizer(PoolAllocator, size_t min, size_t max, size_t step) +{ +export: + PoolAllocator[((max + 1) - min) / step] poolAllocators; + + static if (__traits(hasMember, PoolAllocator, "NeedsLocking")) + { + /// + enum NeedsLocking = PoolAllocator.NeedsLocking; + } + else + { + /// + enum NeedsLocking = false; + } + +scope @trusted @nogc nothrow: + + this(return scope ref Bucketizer other) @trusted + { + foreach (i, ref v; this.poolAllocators) + v = other.poolAllocators[i]; + other = Bucketizer.init; + } + + /// + bool isNull() const + { + foreach (ref bucket; poolAllocators) + if (bucket.isNull) + return true; + return false; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + if (isNull) + return null; + else + { + void[] ret = bucketFor(size).allocate(size, ti); + return ret[0 .. size]; + } + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (!isNull && bucketFor(array.length).reallocate(array, newSize)) + { + array = array[0 .. newSize]; + return true; + } + else + return false; + } + + /// + bool deallocate(scope void[] array) + { + if (isNull) + return false; + + if (bucketFor(array.length).deallocate(array)) + return true; + + foreach (ref bucket; poolAllocators) + { + if (bucket.deallocate(array)) + return true; + } + + return false; + } + + static if (__traits(hasMember, PoolAllocator, "owns")) + { + /// + Ternary owns(scope void[] array) + { + if (isNull) + return Ternary.no; + else + { + if (bucketFor(array.length).owns(array) == Ternary.yes) + return Ternary.yes; + + foreach (ref bucket; poolAllocators) + if (bucket.owns(array) == Ternary.yes) + return Ternary.yes; + return Ternary.no; + } + } + } + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + /// + bool deallocateAll() + { + if (isNull) + return false; + else + { + foreach (ref bucket; poolAllocators) + bucket.deallocateAll(); + return true; + } + } + } + +private @hidden: + ref PoolAllocator bucketFor(size_t size) + { + if (size < min) + return poolAllocators[0]; + else + { + size_t ret = (size - min) / step; + + if (ret >= poolAllocators.length) + ret = poolAllocators.length - 1; + + return poolAllocators[ret]; + } + } +} diff --git a/phobos/sys/allocators/alternatives/fallback.d b/phobos/sys/allocators/alternatives/fallback.d new file mode 100644 index 00000000000..77d5f642a38 --- /dev/null +++ b/phobos/sys/allocators/alternatives/fallback.d @@ -0,0 +1,133 @@ +/** +Attempts to use one memory allocator and if that fails uses another. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.alternatives.fallback; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + alias FBRC = FallbackAllocator!(RCAllocator, RCAllocator); +} + +export: + +/** +A simple fall back allocator, try primary if not try secondary otherwise use primary. + +Does not use `TypeInfo`, but will be forwarded on allocation. +*/ +struct FallbackAllocator(Primary, Secondary) +{ +export: + /// + Primary primary; + /// + Secondary secondary; + + /// + enum NeedsLocking = () { + bool ret; + + static if (__traits(hasMember, Primary, "NeedsLocking")) + if (Primary.NeedsLocking) + ret = true; + static if (__traits(hasMember, Secondary, "NeedsLocking")) + if (Secondary.NeedsLocking) + ret = true; + + return ret; + }(); + +scope @safe @nogc nothrow: + + this(return scope ref FallbackAllocator other) @trusted + { + this.tupleof = other.tupleof; + other = FallbackAllocator.init; + } + + /// + bool isNull() const + { + return primary.isNull || secondary.isNull; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + if (isNull) + return null; + else + { + void[] ret = primary.allocate(size, ti); + + if (ret is null) + ret = secondary.allocate(size, ti); + + return ret; + } + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (isNull) + return false; + else if (primary.owns(array) == Ternary.yes || secondary.owns(array) == Ternary.no) + return primary.reallocate(array, newSize); + else + return secondary.reallocate(array, newSize); + } + + /// + bool deallocate(scope void[] array) + { + if (isNull) + return false; + else if (primary.owns(array) == Ternary.yes || secondary.owns(array) == Ternary.no) + return primary.deallocate(array); + else + return secondary.deallocate(array); + } + + /// + Ternary owns(scope void[] array) + { + if (isNull) + return Ternary.no; + else + return primary.owns(array) == Ternary.yes ? Ternary.yes : secondary.owns(array); + } + + static if (__traits(hasMember, Primary, "deallocateAll") + && __traits(hasMember, Secondary, "deallocateAll")) + { + /// + bool deallocateAll() + { + if (isNull) + return false; + else + { + primary.deallocateAll(); + secondary.deallocateAll(); + return true; + } + } + } + + static if (__traits(hasMember, Primary, "empty") && __traits(hasMember, Secondary, "empty")) + { + /// + bool empty() + { + return isNull || primary.empty() && secondary.empty(); + } + } +} diff --git a/phobos/sys/allocators/alternatives/quantizer.d b/phobos/sys/allocators/alternatives/quantizer.d new file mode 100644 index 00000000000..c5053b2c51a --- /dev/null +++ b/phobos/sys/allocators/alternatives/quantizer.d @@ -0,0 +1,124 @@ +/** +Rounds up memory allocation sizes based upon a size. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.alternatives.quantizer; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + alias QRC = Quantizer!(RCAllocator, length => length * 2); +} + +export: + +/** +Applies rounding (up) function to all sizes provided, but will not return it complete. + +Does not use `TypeInfo`, but will be forwarded on allocation. +*/ +struct Quantizer(PoolAllocator, alias roundFunction) +{ +export: + /// + PoolAllocator poolAllocator; + + static if (__traits(hasMember, PoolAllocator, "NeedsLocking")) + { + /// + enum NeedsLocking = PoolAllocator.NeedsLocking; + } + else + { + /// + enum NeedsLocking = false; + } + +scope @safe @nogc nothrow: + + this(return scope ref Quantizer other) @trusted + { + this.tupleof = other.tupleof; + other = Quantizer.init; + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + if (isNull) + return null; + else + { + void[] ret = poolAllocator.allocate(roundFunction(size), ti); + return ret[0 .. size]; + } + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (!isNull && poolAllocator.reallocate(array, roundFunction(newSize))) + { + array = array[0 .. newSize]; + return true; + } + else + return false; + } + + /// + bool deallocate(scope void[] array) + { + if (isNull) + return false; + else + return poolAllocator.deallocate(array); + } + + static if (__traits(hasMember, PoolAllocator, "owns")) + { + /// + Ternary owns(scope void[] array) + { + if (isNull) + return Ternary.no; + else + return poolAllocator.owns(array); + } + } + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + /// + bool deallocateAll() + { + if (isNull) + return false; + else + return poolAllocator.deallocateAll(); + } + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + if (isNull) + return true; + else + return poolAllocator.empty(); + } + } +} diff --git a/phobos/sys/allocators/alternatives/segregator.d b/phobos/sys/allocators/alternatives/segregator.d new file mode 100644 index 00000000000..3333c70151f --- /dev/null +++ b/phobos/sys/allocators/alternatives/segregator.d @@ -0,0 +1,271 @@ +/** +Allows splitting of allocations between two sizes of allocations. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.alternatives.segregator; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + alias SegRC = Segregator!(RCAllocator, RCAllocator, 1024); +} + +export: + +/** +Splits memory allocations based upon size. Uses small <= threshold < large. + +Does not use `TypeInfo`, but will be forwarded on allocation. +*/ +struct Segregator(SmallAllocator, LargeAllocator, size_t threshold) +{ +export: + /// + SmallAllocator smallAllocator; + /// + LargeAllocator largeAllocator; + + /// + enum NeedsLocking = () { + bool ret; + + static if (__traits(hasMember, SmallAllocator, "NeedsLocking")) + if (SmallAllocator.NeedsLocking) + ret = true; + static if (__traits(hasMember, LargeAllocator, "NeedsLocking")) + if (LargeAllocator.NeedsLocking) + ret = true; + + return ret; + }(); + +scope @safe @nogc nothrow: + + this(return scope ref Segregator other) @trusted + { + this.tupleof = other.tupleof; + other = Segregator.init; + } + + /// + bool isNull() const + { + return smallAllocator.isNull || largeAllocator.isNull; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + if (isNull) + return null; + else + { + if (size <= threshold) + return smallAllocator.allocate(size, ti); + else + return largeAllocator.allocate(size, ti); + } + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (isNull) + return false; + else if (smallAllocator.owns(array) == Ternary.yes) + return smallAllocator.reallocate(array, newSize); + else + return largeAllocator.reallocate(array, newSize); + } + + /// + bool deallocate(scope void[] array) + { + if (isNull) + return false; + else if (smallAllocator.owns(array) == Ternary.yes) + return smallAllocator.deallocate(array); + else + return largeAllocator.deallocate(array); + } + + static if (__traits(hasMember, SmallAllocator, "owns") + && __traits(hasMember, LargeAllocator, "owns")) + { + /// + Ternary owns(scope void[] array) + { + if (isNull) + return Ternary.no; + else if (largeAllocator.owns(array) != Ternary.yes) + return smallAllocator.owns(array); + else + return largeAllocator.owns(array); + } + } + + static if (__traits(hasMember, SmallAllocator, "deallocateAll") + && __traits(hasMember, LargeAllocator, "deallocateAll")) + { + /// + bool deallocateAll() + { + if (isNull) + return false; + else + { + smallAllocator.deallocateAll(); + largeAllocator.deallocateAll(); + return true; + } + } + } + + static if (__traits(hasMember, SmallAllocator, "empty") + && __traits(hasMember, LargeAllocator, "empty")) + { + /// + bool empty() + { + return isNull || smallAllocator.empty() && largeAllocator.empty(); + } + } +} + +/// A segregator based upon the page size with a multiplier +struct SegregatorPageThreshold(SmallAllocator, LargeAllocator, size_t multiplier = 1) +{ +export: + /// + SmallAllocator smallAllocator; + /// + LargeAllocator largeAllocator; + + private + { + size_t threshold; + } + + /// + enum NeedsLocking = () { + bool ret; + + static if (__traits(hasMember, SmallAllocator, "NeedsLocking")) + if (SmallAllocator.NeedsLocking) + ret = true; + static if (__traits(hasMember, LargeAllocator, "NeedsLocking")) + if (LargeAllocator.NeedsLocking) + ret = true; + + return ret; + }(); + +scope @safe @nogc nothrow: + + /// + this(size_t threshold) + { + this.threshold = threshold; + } + + this(return scope ref SegregatorPageThreshold other) @trusted + { + this.tupleof = other.tupleof; + other = SegregatorPageThreshold.init; + } + + /// + bool isNull() const + { + return smallAllocator.isNull || largeAllocator.isNull; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + import phobos.sys.allocators.mapping.vars : PAGESIZE; + + if (threshold == 0) + threshold = multiplier * PAGESIZE; + assert(threshold > 0); + + if (isNull) + return null; + else + { + if (size <= threshold) + return smallAllocator.allocate(size, ti); + else + return largeAllocator.allocate(size, ti); + } + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (isNull) + return false; + else if (smallAllocator.owns(array) == Ternary.yes) + return smallAllocator.reallocate(array, newSize); + else + return largeAllocator.reallocate(array, newSize); + } + + /// + bool deallocate(scope void[] array) + { + if (isNull) + return false; + else if (smallAllocator.owns(array) == Ternary.yes) + return smallAllocator.deallocate(array); + else + return largeAllocator.deallocate(array); + } + + static if (__traits(hasMember, SmallAllocator, "owns") + && __traits(hasMember, LargeAllocator, "owns")) + { + /// + Ternary owns(scope void[] array) + { + if (isNull) + return Ternary.no; + else if (largeAllocator.owns(array) != Ternary.yes) + return smallAllocator.owns(array); + else + return largeAllocator.owns(array); + } + } + + static if (__traits(hasMember, SmallAllocator, "deallocateAll") + && __traits(hasMember, LargeAllocator, "deallocateAll")) + { + /// + bool deallocateAll() + { + if (isNull) + return false; + else + { + smallAllocator.deallocateAll(); + largeAllocator.deallocateAll(); + return true; + } + } + } + + static if (__traits(hasMember, SmallAllocator, "empty") + && __traits(hasMember, LargeAllocator, "empty")) + { + /// + bool empty() + { + return isNull || smallAllocator.empty() && largeAllocator.empty(); + } + } +} diff --git a/phobos/sys/allocators/api.d b/phobos/sys/allocators/api.d new file mode 100644 index 00000000000..82f1541b671 --- /dev/null +++ b/phobos/sys/allocators/api.d @@ -0,0 +1,766 @@ +/** +The main API for memory allocators. + +Posix: On fork will set global allocator to malloc. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole + */ +module phobos.sys.allocators.api; +import phobos.sys.typecons : Ternary; + +version (D_BetterC) +{ +} +else +{ + /// + private + { + import phobos.sys.internal.mutualexclusion; + + __gshared TestTestSetLockInline globalAllocatorLock; + __gshared RCAllocator globalAllocator_; + } + +export: + + /** + Get the global allocator for the process + + Any memory returned can be assumed to be aligned to GoodAlignment or larger. + */ + RCAllocator globalAllocator() @trusted nothrow @nogc + { + globalAllocatorLock.pureLock; + scope (exit) + globalAllocatorLock.unlock; + + if (globalAllocator_.isNull) + { + version (all) + { + import phobos.sys.allocators.predefined; + + globalAllocator_ = RCAllocator.instanceOf!GCAllocator(); + } + else + { + import phobos.sys.allocators.mapping.malloc; + + globalAllocator_ = RCAllocator.instanceOf!Mallocator(); + } + + version (Posix) + { + import phobos.sys.allocators.mapping.malloc; + import core.sys.posix.pthread : pthread_atfork; + + extern (C) static void onForkForGlobalAllocator() + { + globalAllocator_ = RCAllocator.instanceOf!Mallocator(); + } + + // we need to clear out the state due to locks not getting cleared *sigh* + pthread_atfork(null, null, &onForkForGlobalAllocator); + } + } + + return globalAllocator_; + } + + /** + Set the global allocator for the process + + Warning: this allocator MUST return memory aligned to GoodAlignment or larger. + */ + void globalAllocator(RCAllocator allocator) @system nothrow @nogc + { + globalAllocatorLock.pureLock; + scope (exit) + globalAllocatorLock.unlock; + + globalAllocator_ = allocator; + } +} + +/** +Gives a memory allocator suitable to allocate a given type. + +If that memory allocator can contain GC memory, it will be a compatible to GC allocator. + +If GC compatibility is not needed, it will use malloc instead of the general purpose allocator. + +Warning: not all allocators returned by this offer ownership tests. Therefore you must be careful to match deallocation to allocation allocator. +*/ +RCAllocator allocatorGivenType(T)(bool canContainGCMemory = true) +{ + import core.internal.traits : hasIndirections; + + enum HasIndirections = hasIndirections!T; + + static if (HasIndirections) + { + if (canContainGCMemory) + { + import phobos.sys.allocators.predefined; + + return RCAllocator.instanceOf!GCAllocator(); + } + else + { + import phobos.sys.allocators.mapping.malloc; + + return RCAllocator.instanceOf!Mallocator(); + } + } + else + { + import phobos.sys.allocators.mapping.malloc; + + return RCAllocator.instanceOf!Mallocator(); + } +} + +/// +unittest +{ + struct A + { + int x; + } + + struct B + { + int* ptr; + } + + RCAllocator aAllocator = allocatorGivenType!A; + RCAllocator bAllocatorGC = allocatorGivenType!B(true); + RCAllocator bAllocatorNonGC = allocatorGivenType!B(false); + + A* a = aAllocator.make!A; + B* bGC = bAllocatorGC.make!B; + B* bNonGC = bAllocatorNonGC.make!B; + + assert(a !is null); + assert(bGC !is null); + assert(bNonGC !is null); + + aAllocator.dispose(a); + bAllocatorGC.dispose(bGC); + bAllocatorNonGC.dispose(bNonGC); +} + +/// Reference counted memory allocator interface. +struct RCAllocator +{ + private + { + RCAllocatorVtbl* state; + } + +export @safe @nogc nothrow: + + /// Acquire an RCAllocator from a built up memory allocator with support for getting the default instance from its static member. + static RCAllocator instanceOf(T)(RCAllocatorInstance!T* value = defaultInstanceForAllocator!T) @trusted + { + return instanceOf_!(RCAllocatorInstance!T)(value); + } + + private static RCAllocator instanceOf_(T)(T* value) @system + { + if (value is null) + return RCAllocator.init; + + alias Parent = typeof(T.init.parent); + + static if (__traits(hasMember, Parent, "NeedsLocking")) + static assert(!Parent.NeedsLocking, + "An allocator must not require locking to be thread safe. Remove or explicitly lock it to a thread."); + + static assert(__traits(hasMember, T, "allocate"), + "Allocators must be able to allocate memory"); + static assert(__traits(hasMember, T, "deallocate"), + "Allocators must be able to deallocate memory"); + + value.vtbl.deallocate_ = &value.parent.deallocate; + value.vtbl.allocate_ = &value.parent.allocate; + + static if (__traits(hasMember, Parent, "reallocate")) + value.vtbl.reallocate_ = &value.parent.reallocate; + static if (__traits(hasMember, Parent, "owns")) + value.vtbl.owns_ = &value.parent.owns; + static if (__traits(hasMember, Parent, "deallocateAll")) + value.vtbl.deallocateAll_ = &value.parent.deallocateAll; + static if (__traits(hasMember, Parent, "empty")) + value.vtbl.empty_ = &value.parent.empty; + + static if (__traits(hasMember, Parent, "refAdd") && __traits(hasMember, Parent, "refSub")) + { + value.vtbl.refAdd_ = &value.parent.refAdd; + value.vtbl.refSub_ = &value.parent.refSub; + } + else + { + static assert(!(__traits(hasMember, Parent, "refAdd") || __traits(hasMember, Parent, "refSub")), + "You must provide both refAdd and refSub methods for an allocator to be reference counted."); + + } + + RCAllocator ret; + ret.state = &value.vtbl; + return ret; + } + +scope: + + ~this() + { + if (state !is null && state.refSub_ !is null) + state.refSub_(); + } + + this(return scope ref RCAllocator other) @trusted + { + this.tupleof = other.tupleof; + + if (state !is null && state.refAdd_ !is null) + state.refAdd_(); + } + + /// + void opAssign(scope RCAllocator other) @trusted + { + this.destroy; + this.__ctor(other); + } + + /// + bool isNull() const + { + return state is null || state.allocate_ is null || state.deallocate_ is null; + } + + @disable this(ref const RCAllocator other) const; + + @disable void opAssign(scope ref RCAllocator other) const; + @disable void opAssign(scope RCAllocator other) const; + + /// + bool opCast(T : bool)() scope const + { + return !isNull; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + assert(!isNull); + return state.allocate_(size, ti); + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (isNull || state.reallocate_ is null) + return false; + return state.reallocate_(array, newSize); + } + + /// + bool deallocate(scope void[] data) + { + assert(!isNull); + assert(data.ptr !is null); + assert(data.length > 0); + return state.deallocate_(data); + } + + /// + Ternary owns(scope void[] array) + { + if (isNull || state.owns_ is null) + return Ternary.unknown; + return state.owns_(array); + } + + /// + bool deallocateAll() + { + if (isNull || state.deallocateAll_ is null) + return false; + return state.deallocateAll_(); + } + + /// + bool empty() + { + if (isNull || state.empty_ is null) + return false; + return state.empty_(); + } + + /// + string toString() const + { + return isNull ? "null" : "non-null"; + } +} + +/// +unittest +{ + struct Thing + { + int x; + } + + RCAllocator allocator = allocatorGivenType!Thing(); + + Thing* thing = allocator.make!Thing(4); + assert(thing !is null); + assert(thing.x == 4); + + struct Thing2 + { + int call(int a) + { + return a + 3; + } + } + + Thing2* thing2 = allocator.make!Thing2(); + assert(thing2 !is null); + assert(thing2.call(1) == 4); +} + +/// +unittest +{ + RCAllocator allocator = allocatorGivenType!int(); + + int[] data = allocator.makeArray!int(5); + assert(data.length == 5); +} + +/// Wrap any allocator instance with this, provides virtual table storage. +struct RCAllocatorInstance(Parent) +{ + /// + Parent parent; + + alias parent this; + +private: + + RCAllocatorVtbl vtbl; +} + +private struct RCAllocatorVtbl +{ + void delegate() @safe @nogc nothrow refAdd_; + void delegate() @safe @nogc nothrow refSub_; + + void[]delegate(size_t, TypeInfo ti = null) @safe @nogc nothrow allocate_; + bool delegate(scope void[]) @safe @nogc nothrow deallocate_; + bool delegate(scope ref void[], size_t) @safe @nogc nothrow reallocate_; + Ternary delegate(scope void[]) @safe @nogc nothrow owns_; + bool delegate() @safe @nogc nothrow deallocateAll_; + bool delegate() @safe @nogc nothrow empty_; +} + +private template stateSize(T) +{ + import std.traits : Fields, isNested; + + static if (is(T == class) || is(T == interface)) + enum stateSize = __traits(classInstanceSize, T); + else static if (is(T == struct) || is(T == union)) + enum stateSize = Fields!T.length || isNested!T ? T.sizeof : 0; + else static if (is(T == void)) + enum size_t stateSize = 0; + else + enum stateSize = T.sizeof; +} + +/** +Allocate the memory to store an item of type `T` and emplace it based upon the arguments in `Args`. + +Constructor of the type to allocate `T` determines phobos.sys.internal.attributes of this function. +If constructor does not throw, neither does this. + +May be used in BetterC code. + +See_Also: makeBufferedArrays, makeArray +*/ +auto make(T, Allocator, Args...)(scope auto ref Allocator alloc, return scope auto ref Args args) @trusted +{ + import core.lifetime : emplace; + + size_t sizeToAllocate = stateSize!T; + if (sizeToAllocate == 0) + sizeToAllocate = 1; + + version (D_BetterC) + { + void[] array = alloc.allocate(sizeToAllocate); + } + else + { + void[] array = alloc.allocate(sizeToAllocate, typeid(T)); + } + + if (array is null) + return (T*).init; + + static if (is(T == class)) + { + auto ret = cast(T) array.ptr; + } + else + { + auto ret = cast(T*) array.ptr; + } + + assert(ret !is null); + + version (D_BetterC) + { + emplace!T(ret, args); + } + else + { + try + { + emplace!T(ret, args); + } + catch (Exception) + { + alloc.deallocate(array); + ret = null; + } + } + return ret; +} + +/** +Allocate a set of arrays from an allocator that is acting in the form of a buffer for a specific task. + +Like `make` its phobos.sys.internal.attributes are inherited from any constructor called. + +Warning: Each time this is called, deallocation of past allocations will take place, without destructors being run. + +May be used in BetterC code. + +See_Also: make, makeArray +*/ +template makeBufferedArrays(Types...) if (Types.length > 0) +{ + auto makeBufferedArrays(Allocator)(ref Allocator allocator, size_t[] sizes...) + { + assert(sizes.length == Types.length); + + static struct Result + { + MakeAllArray!Types _; + alias _ this; + } + + Result ret; + allocator.deallocateAll; + + static foreach (i; 0 .. Types.length) + ret[i] = allocator.makeArray!(Types[i])(sizes[i]); + + return ret; + } +} + +/// +unittest +{ + import phobos.sys.allocators.predefined; + + MemoryRegionsAllocator!() allocator; + auto got = allocator.makeBufferedArrays!(int, float)(2, 4); + + static assert(is(typeof(got[0]) == int[])); + static assert(is(typeof(got[1]) == float[])); + + assert(got[0].length == 2); + assert(got[1].length == 4); +} + +/** +Allocates an array. + +Like `make` its phobos.sys.internal.attributes are inherited from any constructor called. + +May be used in BetterC code. + +See_Also: make, makeBufferedArrays +*/ +T[] makeArray(T, Allocator)(auto ref Allocator alloc, size_t length) @trusted +{ + import phobos.sys.allocators.utils : fillUninitializedWithInit; + + if (length == 0) + return null; + + static if (T.sizeof <= 1) + { + const sizeToAllocate = length * T.sizeof; + } + else + { + import core.checkedint : mulu; + + bool overflow; + const sizeToAllocate = mulu(length, T.sizeof, overflow); + if (overflow) + return null; + } + + version (D_BetterC) + { + void[] array = alloc.allocate(sizeToAllocate); + } + else + { + void[] array = alloc.allocate(sizeToAllocate, typeid(T[])); + } + + if (array is null) + return null; + else if (array.length < sizeToAllocate) + { + alloc.deallocate(array); + return null; + } + + T[] ret = (cast(T*) array.ptr)[0 .. length]; + fillUninitializedWithInit(ret); + + return ret; +} + +/** +Allocates an array using existing memory as basis. + +Like `make` its phobos.sys.internal.attributes are inherited from any constructor called. + +Warning: the returned slice, may be larger than the length of `initvalues`. + +May be used in BetterC code. + +See_Also: make, makeBufferedArrays +*/ +T[] makeArray(T, Allocator)(auto ref Allocator alloc, const(T)[] initValues) @trusted +{ + import phobos.sys.allocators.utils : fillUninitializedWithInit; + + T[] ret = alloc.makeArray!T(initValues.length); + + if (ret is null) + return null; + else if (ret.length < initValues.length) + { + alloc.deallocate((cast(void*) ret.ptr)[0 .. T.sizeof * ret.length]); + return null; + } + else + { + fillUninitializedWithInit(ret); + + foreach (i, ref v; initValues) + { + ret[i] = *cast(T*)&v; + } + + return ret; + } +} + +/** +Grows an array in size, by the amount in `delta`. + +May be used in BetterC code. + +See_Also: shrinkArray +*/ +bool expandArray(T, Allocator)(auto ref Allocator alloc, scope ref T[] array, size_t delta) @trusted +{ + import phobos.sys.allocators.utils : fillUninitializedWithInit; + + if (delta == 0) + return true; + else if (array is null) + return false; + + size_t originalLength = array.length; + void[] temp = (cast(void*) array.ptr)[0 .. T.sizeof * array.length]; + + if (!alloc.reallocate(temp, temp.length + (T.sizeof * delta))) + { + return false; + } + + array = (cast(T*) temp.ptr)[0 .. originalLength + delta]; + fillUninitializedWithInit(array[originalLength .. $]); + return true; +} + +/// +unittest +{ + import phobos.sys.allocators.predefined; + + MemoryRegionsAllocator!() allocator; + + // 1 byte is too small to allocate for, + // there should be at least 4-8 bytes available to expand into. + + ubyte[] slice = allocator.makeArray!ubyte(1); + assert(slice.length == 1); + assert(allocator.expandArray(slice, 1)); + assert(slice.length == 2); + + allocator.dispose(slice); +} + +/** +Shrinks an array in size, by the amount in `delta`. + +May be used in BetterC code. + +See_Also: expandArray +*/ +bool shrinkArray(T, Allocator)(auto ref Allocator alloc, scope ref T[] array, size_t delta) @trusted +{ + if (delta > array.length) + return false; + + foreach (ref item; array[$ - delta .. $]) + { + item.destroy; + } + + if (delta == array.length) + { + alloc.deallocate(array); + array = null; + return true; + } + + void[] temp = (cast(void*) array.ptr)[0 .. T.sizeof * array.length]; + bool result = alloc.reallocate(temp, temp.length - (delta * T.sizeof)); + array = cast(T[]) temp; + return result; +} + +/// +unittest +{ + import phobos.sys.allocators.predefined; + + MemoryRegionsAllocator!() allocator; + + int[] slice = allocator.makeArray!int(2); + assert(slice.length == 2); + assert(allocator.shrinkArray(slice, 1)); + assert(slice.length == 1); + + allocator.dispose(slice); +} + +/** +Calls destroy on `memory` and then deallocates it. + +May be used in BetterC code. +*/ +void dispose(Type, Allocator)(auto ref Allocator alloc, scope auto ref Type* memory) +{ + void[] toDeallocate = () @trusted { + return (cast(void*) memory)[0 .. Type.sizeof]; + }(); + + destroy(*memory); + alloc.deallocate(toDeallocate); +} + +/// Ditto +void dispose(Type, Allocator)(auto ref Allocator alloc, scope auto ref Type memory) + if (is(Type == class) || is(Type == interface)) +{ + if (memory is null) + return; + + static if (is(Type == interface)) + { + version (Windows) + { + import core.sys.windows.unknwn : IUnknown; + + static assert(!is(T : IUnknown), + "COM interfaces can't be destroyed in " ~ __PRETTY_FUNCTION__); + } + auto ob = cast(Object) memory; + } + else + alias ob = memory; + + void[] toDeallocate = () @trusted { + return (cast(void*) ob)[0 .. typeid(ob).initializer.length]; + }(); + + memory.destroy; + alloc.deallocate(toDeallocate); +} + +/// Ditto +void dispose(Type, Allocator)(auto ref Allocator alloc, scope auto ref Type[] memory) +{ + static if (!is(Type == void)) + { + foreach (ref e; memory) + { + destroy(cast() e); + } + } + + void[] toDeallocate = () @trusted { + return (cast(void*) memory.ptr)[0 .. Type.sizeof * memory.length]; + }(); + + alloc.deallocate(toDeallocate); +} + +private: +import std.meta : AliasSeq; + +RCAllocatorInstance!T* defaultInstanceForAllocator(T)() +{ + import std.traits : isPointer; + + static if (__traits(hasMember, T, "instance")) + { + static if (isPointer!(typeof(__traits(getMember, T, "instance")))) + return T.instance; + else + return &T.instance; + } + else + return null; +} + +template MakeAllArray(Types...) +{ + alias MakeAllArray = AliasSeq!(); + + static foreach (Type; Types) + { + MakeAllArray = AliasSeq!(MakeAllArray, Type[]); + } +} diff --git a/phobos/sys/allocators/buffers/buddylist.d b/phobos/sys/allocators/buffers/buddylist.d new file mode 100644 index 00000000000..02bb321e16b --- /dev/null +++ b/phobos/sys/allocators/buffers/buddylist.d @@ -0,0 +1,447 @@ +/** +A buddy list for general purpose memory allocation. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole + */ +module phobos.sys.allocators.buffers.buddylist; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + alias BL = BuddyList!(RCAllocator); +} + +export: + +/** +The famous buddy list! +It keeps your memory close to its buddies! +Perfact for all of your allocations needs. + +Default exponent range is designed for 32bit coverage, with a maximum allocation size for internal storage at 1gb. + +Set `storeAllocated` to `true` if you need the buddy list to handle getting true range of memory. +This is only required if you do not have another allocator wrapping this one. + +Does not use `TypeInfo`, but will be forwarded on allocation. + +Warning: does not destroy on deallocation. + +https://en.wikipedia.org/wiki/Buddy_memory_allocation +*/ +struct BuddyList(PoolAllocator, size_t minExponent = 3, size_t maxExponent = 20, + bool storeAllocated = false) +{ +export: + static assert(minExponent >= calculatePower2Size((void*).sizeof, 0)[1], + "Minimum exponent must be large enough to fit a pointer in."); + static assert(minExponent < maxExponent, + "Maxinum exponent must be larger than minimum exponent."); + + /// Source for all memory + PoolAllocator poolAllocator; + + /// + enum NeedsLocking = true; + + invariant + { + assert(!poolAllocator.isNull); + + version (none) + { + foreach (offset; 0 .. NumberOfBlocks) + { + Block* current = cast(Block*) blocks[offset]; + + while (current !is null) + { + current = current.next; + } + } + } + } + + private + { + import phobos.sys.allocators.storage.allocatedtree; + + enum NumberOfBlocks = maxExponent - minExponent; + + Block*[NumberOfBlocks] blocks; + + static if (storeAllocated) + { + AllocatedTree!() allocations, fullAllocations; + } + + static struct Block + { + Block* next; + } + } + +scope @safe @nogc nothrow: + + /// + ~this() + { + deallocateAll(); + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + +@trusted: + + /// + this(return scope ref BuddyList other) + { + this.tupleof = other.tupleof; + other.blocks = typeof(blocks).init; + other = BuddyList.init; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + import phobos.sys.allocators.mapping.vars : PAGESIZE; + + void[] splitUntilSize(Block* current, size_t available, size_t blockOffset) + { + void* ret = cast(void*) current; + + while (blockOffset > 0 && available / 2 >= size) + { + assert(blockOffset > 0); + blockOffset--; + available /= 2; + + Block* splitAs = cast(Block*)((cast(void*) current) + available); + + splitAs.next = blocks[blockOffset]; + blocks[blockOffset] = splitAs; + } + + assert(available >= size); + return ret[0 .. available]; + } + + size_t[2] blockSizeAndOffsetSource = calculatePower2Size(size, minExponent); + void[] ret; + + if (blockSizeAndOffsetSource[1] < NumberOfBlocks) + { + size_t[2] blockSizeAndOffset = blockSizeAndOffsetSource; + Block** parent = &blocks[blockSizeAndOffset[1]]; + + while (blockSizeAndOffset[1] + 1 < NumberOfBlocks && *parent is null) + { + parent++; + blockSizeAndOffset[0] *= 2; + blockSizeAndOffset[1]++; + } + + assert(blockSizeAndOffset[0] < 2 ^^ maxExponent); + assert(blockSizeAndOffset[1] < maxExponent); + if (blockSizeAndOffset[1] < NumberOfBlocks && *parent !is null) + { + Block* got = *parent; + *parent = got.next; + ret = splitUntilSize(got, blockSizeAndOffset[0], blockSizeAndOffset[1]); + } + } + + if (ret is null) + { + enum MaxShouldPower2 = 2 * 1024 * 1024 * 1024; + + size_t allocateSize = PAGESIZE(); + if (allocateSize < size) + allocateSize = size; + if (allocateSize < MaxShouldPower2 && allocateSize < blockSizeAndOffsetSource[0]) + allocateSize = blockSizeAndOffsetSource[0]; + + ret = poolAllocator.allocate(allocateSize, ti); + + if (ret !is null) + { + static if (storeAllocated) + { + fullAllocations.store(ret); + } + } + else + assert(0); + } + + if (ret !is null) + { + assert(ret.length >= size); + + static if (storeAllocated) + { + allocations.store(ret); + } + + return ret; + } + else + return null; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + static if (storeAllocated) + { + if (void[] trueArray = allocations.getTrueRegionOfMemory(array)) + { + if (trueArray.length >= newSize) + { + array = trueArray[0 .. newSize]; + return true; + } + } + } + + return false; + } + + /// + bool deallocate(scope void[] array) + { + void[] trueArray = array; + + static if (storeAllocated) + { + trueArray = allocations.getTrueRegionOfMemory(array); + } + + if (trueArray) + { + size_t[2] blockSizeAndOffset = calculatePower2Size(trueArray.length, minExponent); + + static if (storeAllocated) + { + allocations.remove(trueArray); + } + + if (blockSizeAndOffset[1] >= NumberOfBlocks) + { + static if (storeAllocated) + { + fullAllocations.remove(trueArray); + } + + poolAllocator.deallocate(trueArray); + } + else + { + void[] fullSizeArray = trueArray; + + static if (storeAllocated) + { + fullSizeArray = fullAllocations.getTrueRegionOfMemory(trueArray); + } + + Loop: do + { + void* startOfPrevious, startOfNext; + + if (trueArray.ptr > fullSizeArray.ptr + && cast(size_t) trueArray.ptr > trueArray.length) + startOfPrevious = trueArray.ptr - trueArray.length; + + if (trueArray.ptr + trueArray.length < fullSizeArray.ptr + fullSizeArray.length) + startOfNext = trueArray.ptr + trueArray.length; + + Block** parent = &blocks[blockSizeAndOffset[1]]; + const taL2 = trueArray.length * 2; + + for (;;) + { + Block* current = *parent; + if (current is null) + break Loop; + + assert(current.next is null || current.next.next is null + || current.next.next !is null); + + if (current is startOfPrevious) + { + trueArray = startOfPrevious[0 .. taL2]; + *parent = current.next; + break; + } + else if (current is startOfNext) + { + trueArray = trueArray.ptr[0 .. taL2]; + *parent = current.next; + break; + } + + parent = ¤t.next; + } + + blockSizeAndOffset[0] *= 2; + blockSizeAndOffset[1]++; + } + while (blockSizeAndOffset[1] + 1 < NumberOfBlocks); + + { + void[] trueArrayOrigin = trueArray; + + static if (storeAllocated) + { + trueArrayOrigin = fullAllocations.getTrueRegionOfMemory(trueArray); + } + + if (trueArrayOrigin.ptr is trueArray.ptr + && trueArrayOrigin.length == trueArray.length) + { + static if (storeAllocated) + { + fullAllocations.remove(trueArray); + } + + poolAllocator.deallocate(trueArray); + } + else + { + Block* blockToAdd = cast(Block*) trueArray.ptr; + blockToAdd.next = blocks[blockSizeAndOffset[1]]; + + blocks[blockSizeAndOffset[1]] = blockToAdd; + } + } + } + + return true; + } + + return false; + } + + /// + Ternary owns(scope void[] array) + { + static if (storeAllocated) + { + return allocations.owns(array); + } + else + { + return poolAllocator.owns(array); + } + } + + /// + bool deallocateAll() + { + static if (storeAllocated) + { + allocations.deallocateAll(null); + fullAllocations.deallocateAll(&poolAllocator.deallocate); + } + else + { + poolAllocator.deallocateAll(); + } + + blocks = typeof(blocks).init; + return true; + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + return blocks[$ - 1] is null && poolAllocator.empty(); + } + } +} + +/// +unittest +{ + import phobos.sys.allocators.mapping.malloc; + import phobos.sys.allocators.buffers.region; + + alias BL = BuddyList!(Region!Mallocator, 3, 20, true); + + BL bl; + assert(!bl.empty); + assert(!bl.isNull); + + bl = BL(); + assert(!bl.empty); + assert(!bl.isNull); + + auto tempAllocation = bl.poolAllocator.allocate(1024 * 1024); + bl.poolAllocator.deallocate(tempAllocation[1 .. $]); + + void[] got1 = bl.allocate(1024); + assert(got1 !is null); + assert(got1.length >= 1024); + assert(bl.owns(null) == Ternary.no); + assert(bl.owns(got1) == Ternary.yes); + assert(bl.owns(got1[10 .. 20]) == Ternary.yes); + + void[] got2 = bl.allocate(512); + assert(got2 !is null); + assert(got2.length >= 512); + assert(bl.owns(null) == Ternary.no); + assert(bl.owns(got2) == Ternary.yes); + assert(bl.owns(got2[10 .. 20]) == Ternary.yes); + + void[] got3 = bl.allocate(1024); + assert(got3 !is null); + assert(got3.length >= 1024); + assert(bl.owns(null) == Ternary.no); + assert(bl.owns(got3) == Ternary.yes); + assert(bl.owns(got3[10 .. 20]) == Ternary.yes); + + bool success = bl.reallocate(got1, 2048); + assert(success); + assert(got1.length >= 2048); + + success = bl.deallocate(got1); + assert(success); + success = bl.deallocate(got2); + assert(success); + success = bl.deallocate(got3); + assert(success); + + got1 = bl.allocate(512); + assert(got1 !is null); + assert(got1.length >= 512); + assert(bl.owns(null) == Ternary.no); + assert(bl.owns(got1) == Ternary.yes); + assert(bl.owns(got1[10 .. 20]) == Ternary.yes); +} + +private @hidden: + +size_t[2] calculatePower2Size()(size_t requested, size_t minExponent) @safe nothrow @nogc +{ + size_t value = 1, power; + + while (value < requested || power < minExponent) + { + value <<= 1; + power++; + } + + power -= minExponent; + return [value, power]; +} diff --git a/phobos/sys/allocators/buffers/defs.d b/phobos/sys/allocators/buffers/defs.d new file mode 100644 index 00000000000..eb86bd769e8 --- /dev/null +++ b/phobos/sys/allocators/buffers/defs.d @@ -0,0 +1,21 @@ +/** +Definitions used by multiple allocator buffers. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.buffers.defs; + +/// +enum FitsStrategy +{ + /// + FirstFit, + /// + NextFit, + /// + BestFit, + /// + WorstFit, +} diff --git a/phobos/sys/allocators/buffers/freelist.d b/phobos/sys/allocators/buffers/freelist.d new file mode 100644 index 00000000000..ca16938e793 --- /dev/null +++ b/phobos/sys/allocators/buffers/freelist.d @@ -0,0 +1,702 @@ +/** +List based memory allocation and storage strategies. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.buffers.freelist; +import phobos.sys.allocators.mapping : GoodAlignment; +public import phobos.sys.allocators.buffers.defs : FitsStrategy; +public import phobos.sys.allocators.predefined : HouseKeepingAllocator; +import phobos.sys.allocators.storage.allocatedlist; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + // guarantee tha each strategy has been initialized + alias HCFreeList = HouseKeepingFreeList!(RCAllocator); + + alias FreeListFirstFit = FreeList!(RCAllocator, FitsStrategy.FirstFit); + alias FreeListNextFit = FreeList!(RCAllocator, FitsStrategy.NextFit); + alias FreeListBestFit = FreeList!(RCAllocator, FitsStrategy.BestFit); + alias FreeListWorstFit = FreeList!(RCAllocator, FitsStrategy.WorstFit); + +} + +export: + +/** +A free list dedicated for house keeping tasks. + +Fixed sized allocations, does not handle alignment, coalesce blocks of memory together nor splitting. + +Does not use `TypeInfo`, but will be forwarded on allocation. +*/ +struct HouseKeepingFreeList(PoolAllocator) +{ +export: + /// Source for all memory + PoolAllocator poolAllocator; + + /// + enum NeedsLocking = true; + + invariant + { + assert(head.next is null || !poolAllocator.isNull); + + version (none) + { + Node* current = cast(Node*) head.next; + while (current !is null) + current = current.next; + } + } + + private + { + Node head; + } + +scope @safe @nogc nothrow: + + /// + ~this() + { + Node* current = head.next; + + while (current !is null) + { + Node* next = current.next; + + poolAllocator.deallocate(current.recreate()); + + current = next; + } + + head.next = null; + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + +@trusted: + + /// + this(return scope ref HouseKeepingFreeList other) + { + this.tupleof = other.tupleof; + other.head.next = null; + other = HouseKeepingFreeList.init; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + Node* current = head.next; + + if (current !is null && current.available >= size) + return listAllocate(&head, current, size); + else + { + size_t toAllocate = size < Node.sizeof ? Node.sizeof : size; + void[] ret = poolAllocator.allocate(toAllocate, ti); + + if (ret !is null) + return ret[0 .. size]; + else + return null; + } + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + return poolAllocator.reallocate(array, newSize); + } + + /// + bool deallocate(scope void[] data) + { + if (data is null) + return false; + + Node* node = cast(Node*) data.ptr; + node.available = data.length; + node.next = head.next; + + head.next = node; + return true; + } + + static if (__traits(hasMember, PoolAllocator, "owns")) + { + /// + Ternary owns(scope void[] array) + { + return poolAllocator.owns(array); + } + } + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + /// + bool deallocateAll() + { + if (poolAllocator.deallocateAll()) + { + head.next = null; + return true; + } + + return false; + } + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + return poolAllocator.empty(); + } + } + +private: + static struct Node + { + Node* next; + size_t available; + + @safe @nogc scope nothrow @hidden: + + void[] recreate() @trusted + { + return (cast(void*)&this)[0 .. available]; + } + } + + void[] listAllocate(Node* previous, Node* current, size_t size) + { + previous.next = current.next; + return current.recreate()[0 .. size]; + } +} + +/// +unittest +{ + import phobos.sys.allocators.mapping.malloc; + import phobos.sys.allocators.buffers.region; + + alias HK = HouseKeepingFreeList!(Region!Mallocator); + + HK hk; + assert(!hk.empty); + assert(!hk.isNull); + + hk = HK(); + assert(!hk.empty); + assert(!hk.isNull); + + void[] got = hk.allocate(1024); + assert(got !is null); + assert(got.length == 1024); + assert(hk.owns(got) == Ternary.yes); + assert(hk.owns(got[10 .. 20]) == Ternary.yes); + + bool success = hk.reallocate(got, 2048); + assert(success); + assert(got.length == 2048); + + assert(hk.owns(null) == Ternary.no); + assert(hk.owns(got) == Ternary.yes); + assert(hk.owns(got[10 .. 20]) == Ternary.yes); + + success = hk.deallocate(got); + assert(success); +} + +/** +A simple straight forward free list that supports first-fit, next-fit and best-fit strategies, with optional alignment and minimum stored size. + +This is not designed to be fast, it exists to be a base line. Use FreeTree if you care about performance. + +Does not use `TypeInfo`, but will be forwarded on allocation. + +Warning: does not destroy on deallocation. + +See_Also: FreeTree +*/ +struct FreeList(PoolAllocator, FitsStrategy Strategy = FitsStrategy.NextFit, + size_t DefaultAlignment = GoodAlignment, size_t DefaultMinimumStoredSize = 0) +{ +export: + /// Source for all memory + PoolAllocator poolAllocator; + /// Ensure all return pointers from stored source are aligned to a multiply of this + size_t alignedTo = DefaultAlignment; + // Ensure all memory stored are at least this size + size_t minimumStoredSize = DefaultMinimumStoredSize; + + /// + enum NeedsLocking = true; + + invariant + { + assert(alignedTo > 0); + assert(head.next is null || !poolAllocator.isNull); + } + + private + { + Node head; + + static if (Strategy == FitsStrategy.NextFit) + { + Node* previous; + } + + AllocatedList!() allocations, fullAllocations; + } + +scope @safe @nogc nothrow: + + /// + ~this() + { + deallocateAll(); + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + +@trusted: + + /// + this(return scope ref FreeList other) + { + this.tupleof = other.tupleof; + other.head = Node.init; + static if (Strategy == FitsStrategy.NextFit) + other.previous = null; + other = FreeList.init; + } + + static if (Strategy == FitsStrategy.FirstFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + Node* previous = &head; + + for (;;) + { + assert(previous !is null); + Node* current = previous.next; + + if (current is null) + { + size_t toAllocateSize = size; + if (size < Node.sizeof) + toAllocateSize = Node.sizeof; + + auto ret = poolAllocator.allocate(toAllocateSize, ti); + if (ret is null) + return null; + + if (ret.length < toAllocateSize) + { + poolAllocator.deallocate(ret); + return null; + } + + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + else if (!current.fitsAlignment(size, alignedTo)) + previous = current.next; + else + return listAllocate(previous, current, size); + } + + assert(0); + } + } + else static if (Strategy == FitsStrategy.NextFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + Node* start = previous; + + for (;;) + { + Node* current; + + if (start is null) + { + head.next = &head; + + previous = &head; + start = previous; + current = start.next; + } + else + current = start.next; + + if (previous is start) + { + size_t toAllocateSize = size; + if (size < Node.sizeof) + toAllocateSize = Node.sizeof; + + auto ret = poolAllocator.allocate(toAllocateSize, ti); + if (ret is null) + return null; + + if (ret.length < toAllocateSize) + { + poolAllocator.deallocate(ret); + return null; + } + + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + else if (!current.fitsAlignment(size, alignedTo)) + previous = current.next; + else + return listAllocate(previous, current, size); + } + + assert(0); + } + } + else static if (Strategy == FitsStrategy.BestFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + Node* best, previous = &head, bestPrevious; + size_t bestSize = size_t.max; + + while (previous !is null) + { + Node* current = previous.next; + + if (current is null || current.available == size) + { + if (current !is null) + { + bestPrevious = previous; + best = current; + } + + break; + } + else if (current.fitsAlignment(size, alignedTo) && bestSize > current.available) + { + assert(best !is current); + best = current; + bestPrevious = previous; + bestSize = current.available; + } + + previous = current.next; + } + + if (best !is null) + return listAllocate(bestPrevious, best, size); + + { + size_t toAllocateSize = size; + if (size < Node.sizeof) + toAllocateSize = Node.sizeof; + + auto ret = poolAllocator.allocate(toAllocateSize, ti); + if (ret is null) + return null; + + if (ret.length < toAllocateSize) + { + poolAllocator.deallocate(ret); + return null; + } + + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + } + } + else static if (Strategy == FitsStrategy.WorstFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + Node* previous = &head, largest, largestPrevious; + size_t largestSize; + + while (previous.next !is null) + { + Node* current = previous.next; + + if (current.available > largestSize) + { + largestSize = current.available; + largest = current; + largestPrevious = previous; + } + + previous = current; + } + + if (largestSize < size) + { + size_t toAllocateSize = size; + if (size < Node.sizeof) + toAllocateSize = Node.sizeof; + + auto ret = poolAllocator.allocate(toAllocateSize, ti); + if (ret is null) + return null; + + if (ret.length < toAllocateSize) + { + poolAllocator.deallocate(ret); + return null; + } + + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + else + return listAllocate(largestPrevious, largest, size); + } + } + else + static assert(0, "Unimplemented fit strategy"); + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (void[] actual = allocations.getTrueRegionOfMemory(array)) + { + size_t pointerDifference = array.ptr - actual.ptr; + size_t amountLeft = actual.length - pointerDifference; + + if (amountLeft >= newSize) + { + array = array.ptr[0 .. newSize]; + return true; + } + } + + return false; + } + + /// + bool deallocate(scope void[] array) + { + void[] trueArray = allocations.getTrueRegionOfMemory(array); + + if (trueArray !is null) + { + allocations.remove(trueArray); + + if (trueArray.length >= Node.sizeof) + { + Node* node = cast(Node*) trueArray.ptr; + node.available = trueArray.length; + + assert(head.next !is node); + node.next = head.next; + head.next = node; + return true; + } + } + + return false; + } + + /// + Ternary owns(scope void[] array) + { + return fullAllocations.owns(array); + } + + /// + bool deallocateAll() + { + allocations.deallocateAll(null); + fullAllocations.deallocateAll(&poolAllocator.deallocate); + + { + head.next = null; + + static if (Strategy == FitsStrategy.NextFit) + { + previous = null; + } + } + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + poolAllocator.deallocateAll(); + } + + return true; + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + return poolAllocator.empty(); + } + } + +private @hidden: + static struct Node + { + Node* next; + size_t available; + + @safe @nogc scope nothrow @hidden: + + void[] recreate() @trusted + { + return (cast(void*)&this)[0 .. available]; + } + + bool fitsAlignment(size_t needed, size_t alignedTo) @trusted + { + if (alignedTo == 0) + return true; + + size_t padding = alignedTo - ((cast(size_t)&this) % alignedTo); + if (padding == alignedTo) + padding = 0; + + return needed + padding <= available; + } + } + + void[] listAllocate(Node* previous, Node* current, size_t size) + { + Node* result = current; + + size_t toAddAlignment = alignedTo - ((cast(size_t) result) % alignedTo); + + if (toAddAlignment == alignedTo) + toAddAlignment = 0; + + size_t remainderAvailable = current.available - (toAddAlignment + size); + + if (shouldSplit(current.available, size, alignedTo) + && remainderAvailable >= minimumStoredSize) + { + allocations.store(result.recreate()[toAddAlignment .. toAddAlignment + size]); + Node* remainder = cast(Node*)&result.recreate()[toAddAlignment + size]; + + remainder.next = result.next; + remainder.available = remainderAvailable; + previous.next = remainder; + } + else + { + allocations.store(result.recreate()); + previous.next = current.next; + } + + return result.recreate()[toAddAlignment .. toAddAlignment + size]; + } +} + +/// +unittest +{ + import phobos.sys.allocators.mapping.malloc; + import phobos.sys.allocators.buffers.region; + + void perform(FL)() + { + FL fl; + assert(!fl.empty); + assert(!fl.isNull); + + fl = FL(); + assert(!fl.empty); + assert(!fl.isNull); + + void[] got1 = fl.allocate(1024); + assert(got1 !is null); + assert(got1.length == 1024); + assert(fl.owns(null) == Ternary.no); + assert(fl.owns(got1) == Ternary.yes); + assert(fl.owns(got1[10 .. 20]) == Ternary.yes); + + void[] got2 = fl.allocate(512); + assert(got2 !is null); + assert(got2.length == 512); + assert(fl.owns(null) == Ternary.no); + assert(fl.owns(got2) == Ternary.yes); + assert(fl.owns(got2[10 .. 20]) == Ternary.yes); + + void[] got3 = fl.allocate(1024); + assert(got3 !is null); + assert(got3.length == 1024); + assert(fl.owns(null) == Ternary.no); + assert(fl.owns(got3) == Ternary.yes); + assert(fl.owns(got3[10 .. 20]) == Ternary.yes); + + bool success = fl.reallocate(got1, 2048); + assert(!success); + assert(got1.length == 1024); + + success = fl.deallocate(got1); + assert(success); + success = fl.deallocate(got2); + assert(success); + success = fl.deallocate(got3); + assert(success); + + got1 = fl.allocate(512); + assert(got1 !is null); + assert(got1.length == 512); + assert(fl.owns(null) == Ternary.no); + assert(fl.owns(got1) == Ternary.yes); + assert(fl.owns(got1[10 .. 20]) == Ternary.yes); + } + + perform!(FreeList!(Region!Mallocator, FitsStrategy.FirstFit)); + perform!(FreeList!(Region!Mallocator, FitsStrategy.NextFit)); + perform!(FreeList!(Region!Mallocator, FitsStrategy.BestFit)); + perform!(FreeList!(Region!Mallocator, FitsStrategy.WorstFit)); +} + +private @hidden: + +bool shouldSplit()(size_t poolSize, size_t forSize, size_t alignment) @safe @nogc nothrow pure +{ + if (alignment == 0) + alignment = (void*).sizeof * 2; + + forSize += (void*).sizeof * 4; + return (poolSize + alignment) > forSize; +} diff --git a/phobos/sys/allocators/buffers/freetree.d b/phobos/sys/allocators/buffers/freetree.d new file mode 100644 index 00000000000..dc1da0bf3de --- /dev/null +++ b/phobos/sys/allocators/buffers/freetree.d @@ -0,0 +1,788 @@ +/** +Tree based memory allocation and storage strategies. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.buffers.freetree; +import phobos.sys.allocators.mapping : GoodAlignment; +public import phobos.sys.allocators.buffers.defs : FitsStrategy; +public import phobos.sys.allocators.predefined : HouseKeepingAllocator; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + // guarantee tha each strategy has been initialized + alias FreeTreeFirstFit = FreeTree!(RCAllocator, FitsStrategy.FirstFit); + alias FreeTreeNextFit = FreeTree!(RCAllocator, FitsStrategy.NextFit); + alias FreeTreeBestFit = FreeTree!(RCAllocator, FitsStrategy.BestFit); + alias FreeTreeWorstFit = FreeTree!(RCAllocator, FitsStrategy.WorstFit); +} + +export: + +/** +An implementation of cartesian tree for storing free memory with optional alignment and minimum stored size. + +Based upon Fast Fits by C. J. Stephenson. http://sigops.org/s/conferences/sosp/2015/archive/1983-Bretton_Woods/06-stephenson-SOSP.pdf + +Will automatically deallocate memory back to the pool allocator when matching original allocation. + +Set `storeAllocated` to `true` if you need the free tree to handle getting true range of memory. +This is only required if you do not have another allocator wrapping this one. + +Does not use `TypeInfo`, but will be forwarded on allocation. + +Warning: does not destroy on deallocation. + +See_Also: FreeList +*/ +struct FreeTree(PoolAllocator, FitsStrategy Strategy, size_t DefaultAlignment = GoodAlignment, + size_t DefaultMinimumStoredSize = 0, bool storeAllocated = false) +{ +export: + /// Source for all memory + PoolAllocator poolAllocator; + /// Ensure all return pointers from stored source are aligned to a multiply of this + size_t alignedTo = DefaultAlignment; + // Ensure all memory stored are at least this size + size_t minimumStoredSize = DefaultMinimumStoredSize; + + /// + enum NeedsLocking = true; + + invariant + { + assert(alignedTo > 0); + assert(anchor is null || !poolAllocator.isNull); + + version (none) + { + void handle(Node* parent) + { + assert(parent.length >= Node.sizeof); + + if (parent.left !is null) + handle(parent.left); + if (parent.right !is null) + handle(parent.right); + } + + if (anchor !is null) + handle(cast(Node*) anchor); + } + } + + private + { + Node* anchor; + + static if (Strategy == FitsStrategy.NextFit) + { + Node** previousAnchor; + } + + static if (storeAllocated) + { + import phobos.sys.allocators.storage.allocatedtree; + + AllocatedTree!() allocations, fullAllocations; + } + } + +@safe @nogc scope nothrow: + + ~this() + { + deallocateAll(); + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + +@trusted: + + /// + this(return scope ref FreeTree other) + { + this.tupleof = other.tupleof; + + other.anchor = null; + static if (Strategy == FitsStrategy.NextFit) + other.previousAnchor = null; + + other = FreeTree.init; + } + + static if (Strategy == FitsStrategy.FirstFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + const actualSizeNeeded = size >= Node.sizeof ? size : Node.sizeof; + Node** parent = &anchor; + + if (*parent is null) + { + auto ret = poolAllocator.allocate(actualSizeNeeded, ti); + if (ret is null) + return null; + + if (ret.length < actualSizeNeeded) + { + poolAllocator.deallocate(ret); + return null; + } + + static if (storeAllocated) + { + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + else + return ret; + } + + Node** currentParent = parent; + Node** left = &(*currentParent).left; + + while (fitsAlignment(left, actualSizeNeeded, alignedTo)) + { + parent = currentParent; + currentParent = left; + left = &(*currentParent).left; + } + + return allocateImpl(actualSizeNeeded, parent); + } + } + else static if (Strategy == FitsStrategy.NextFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + const actualSizeNeeded = size >= Node.sizeof ? size : Node.sizeof; + + void[] perform(scope Node** parent) + { + Node** currentParent = parent, left = &(*currentParent).left; + + while (fitsAlignment(left, actualSizeNeeded, alignedTo)) + { + parent = currentParent; + currentParent = left; + left = &(*currentParent).left; + } + + previousAnchor = parent; + return allocateImpl(actualSizeNeeded, parent); + } + + if (fitsAlignment(previousAnchor, actualSizeNeeded, alignedTo)) + return perform(previousAnchor); + else if (fitsAlignment(&anchor, actualSizeNeeded, alignedTo)) + return perform(&anchor); + + { + auto ret = poolAllocator.allocate(actualSizeNeeded, ti); + if (ret is null) + return null; + + if (ret.length < actualSizeNeeded) + { + poolAllocator.deallocate(ret); + return null; + } + + static if (storeAllocated) + { + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + else + return ret; + } + } + } + else static if (Strategy == FitsStrategy.BestFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + Node** parent = &anchor, currentParent = parent; + + const actualSizeNeeded = size >= Node.sizeof ? size : Node.sizeof; + + if (*currentParent !is null) + { + Node** left = &(*currentParent).left, right = &(*currentParent).right; + bool leftFit = fitsAlignment(left, actualSizeNeeded, alignedTo), + rightFit = fitsAlignment(right, actualSizeNeeded, alignedTo); + + while (leftFit || rightFit) + { + parent = currentParent; + + if (leftFit) + currentParent = left; + else + currentParent = right; + + left = &(*currentParent).left; + right = &(*currentParent).right; + leftFit = fitsAlignment(left, actualSizeNeeded, alignedTo); + rightFit = fitsAlignment(right, actualSizeNeeded, alignedTo); + } + + if (fitsAlignment(parent, actualSizeNeeded, alignedTo)) + { + auto ret = allocateImpl(actualSizeNeeded, parent); + assert(ret.length >= actualSizeNeeded); + return ret; + } + } + + { + auto ret = poolAllocator.allocate(actualSizeNeeded, ti); + if (ret is null) + return null; + + if (ret.length < actualSizeNeeded) + { + poolAllocator.deallocate(ret); + return null; + } + + assert(ret.length >= actualSizeNeeded); + + static if (storeAllocated) + { + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + else + return ret; + } + } + } + else static if (Strategy == FitsStrategy.WorstFit) + { + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + const actualSizeNeeded = size >= Node.sizeof ? size : Node.sizeof; + + if (anchor !is null && fitsAlignment(&anchor, actualSizeNeeded, alignedTo)) + return allocateImpl(actualSizeNeeded, &anchor); + + { + auto ret = poolAllocator.allocate(actualSizeNeeded, ti); + if (ret is null) + return null; + + if (ret.length < actualSizeNeeded) + { + poolAllocator.deallocate(ret); + return null; + } + + static if (storeAllocated) + { + allocations.store(ret); + fullAllocations.store(ret); + return ret[0 .. size]; + } + else + return ret; + } + } + } + else + static assert(0, "Unimplemented fit strategy"); + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + static if (storeAllocated) + { + void[] actual = allocations.getTrueRegionOfMemory(array); + + if (actual) + { + const pointerDifference = array.ptr - actual.ptr; + const lengthAvailable = actual.length - pointerDifference; + + if (lengthAvailable >= newSize) + { + array = array.ptr[0 .. newSize]; + return true; + } + } + } + + return false; + } + + /// + bool deallocate(void[] array) + { + scope trueArray = array; + + static if (storeAllocated) + { + trueArray = allocations.getTrueRegionOfMemory(array); + } + + if (trueArray !is null) + { + assert(trueArray.length >= Node.sizeof); + + static if (storeAllocated) + { + allocations.remove(trueArray); + } + + Node** parent = &anchor; + Node* current; + + while ((current = *parent) !is null) + { + void* currentPtr = cast(void*) current; + + if (currentPtr + current.length is trueArray.ptr) + { + trueArray = currentPtr[0 .. current.length + trueArray.length]; + delete_(parent); + } + else if (trueArray.ptr + trueArray.length is currentPtr) + { + trueArray = trueArray.ptr[0 .. trueArray.length + current.length]; + delete_(parent); + } + else if (trueArray.ptr < currentPtr) + parent = ¤t.left; + else + parent = ¤t.right; + } + + assert(trueArray.length > 0); + void[] trueArrayOrigin = trueArray; + + static if (storeAllocated) + { + trueArrayOrigin = fullAllocations.getTrueRegionOfMemory(trueArray); + } + + if (trueArrayOrigin.ptr is trueArray.ptr && trueArrayOrigin.length == trueArray.length) + { + static if (storeAllocated) + { + fullAllocations.remove(trueArray); + } + + poolAllocator.deallocate(trueArray); + } + else + { + Node* nodeToInsert = cast(Node*) trueArray.ptr; + nodeToInsert.length = trueArray.length; + nodeToInsert.left = null; + nodeToInsert.right = null; + + insert(nodeToInsert, &anchor); + } + + return true; + } + + return false; + } + + /// + Ternary owns(scope void[] array) + { + static if (storeAllocated) + { + return fullAllocations.owns(array); + } + else + return poolAllocator.owns(array); + } + + /// + bool deallocateAll() + { + static if (storeAllocated) + { + allocations.deallocateAll(null); + fullAllocations.deallocateAll(&poolAllocator.deallocate); + } + + anchor = null; + + static if (Strategy == FitsStrategy.NextFit) + { + previousAnchor = null; + } + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + poolAllocator.deallocateAll(); + } + + return true; + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + return poolAllocator.empty(); + } + } + +private @hidden: + void insert(Node* toInsert, Node** parent) + { + assert(toInsert !is null); + assert(toInsert.length >= Node.sizeof); + assert(toInsert.left is null || toInsert.left.length >= Node.sizeof); + assert(toInsert.right is null || toInsert.right.length >= Node.sizeof); + + if (*parent !is null) + { + assert((*parent).length > Node.sizeof); + assert((*parent).left is null || (*parent).left.length >= Node.sizeof); + assert((*parent).right is null || (*parent).right.length >= Node.sizeof); + } + + Node* currentChild = *parent; + + // find parent to inject into + { + while (weightOf(currentChild) >= toInsert.length) + { + if (toInsert < currentChild) + parent = ¤tChild.left; + else + parent = ¤tChild.right; + currentChild = *parent; + } + + *parent = toInsert; + } + + // recombine orphaned nodes back into the tree + { + Node** left_hook = &toInsert.left; + Node** right_hook = &toInsert.right; + + while (currentChild !is null) + { + if (currentChild < toInsert) + { + *left_hook = currentChild; + left_hook = ¤tChild.right; + currentChild = currentChild.right; + } + else + { + *right_hook = currentChild; + right_hook = ¤tChild.left; + currentChild = currentChild.left; + } + } + + *left_hook = null; + *right_hook = null; + } + } + + void delete_(Node** parent) + { + assert(*parent !is null); + assert((*parent).length >= Node.sizeof); + assert((*parent).left is null || (*parent).left.length >= Node.sizeof); + assert((*parent).right is null || (*parent).right.length >= Node.sizeof); + + Node* left = (*parent).left, right = (*parent).right; + size_t weightOfLeft = weightOf(left), weightOfRight = weightOf(right); + + while (left !is right) + { + if (weightOfLeft >= weightOfRight) + { + *parent = left; + parent = &left.right; + + left = left.right; + weightOfLeft = weightOf(left); + } + else + { + *parent = right; + parent = &right.left; + + right = right.left; + weightOfRight = weightOf(right); + } + } + + *parent = null; + } + + void promote(Node* childToPromote, Node** parent) + { + assert(childToPromote !is null); + assert(childToPromote.length >= Node.sizeof); + assert(childToPromote.left is null || childToPromote.left.length >= Node.sizeof); + assert(childToPromote.right is null || childToPromote.right.length >= Node.sizeof); + + Node* currentChild = *parent; + + // finds appropriete parent to inject childToPromote into + { + size_t childToPromoteWeight = weightOf(childToPromote); + + while (weightOf(currentChild) >= childToPromoteWeight) + { + if (childToPromote < currentChild) + parent = ¤tChild.left; + else + parent = ¤tChild.right; + currentChild = *parent; + } + + *parent = childToPromote; + } + + // recombine orphaned nodes back into the tree + { + Node* left_branch = childToPromote.left; + Node* right_branch = childToPromote.right; + Node** left_hook = &childToPromote.left; + Node** right_hook = &childToPromote.right; + + while (currentChild !is childToPromote) + { + if (currentChild < childToPromote) + { + *left_hook = currentChild; + left_hook = ¤tChild.right; + currentChild = currentChild.right; + } + else + { + *right_hook = currentChild; + right_hook = ¤tChild.left; + currentChild = currentChild.left; + } + } + + *left_hook = left_branch; + *right_hook = right_branch; + } + } + + void demote(Node** parent) + { + Node* toDemote = *parent; + assert(toDemote !is null); + assert(toDemote.length >= Node.sizeof); + assert(toDemote.left is null || toDemote.left.length >= Node.sizeof); + assert(toDemote.right is null || toDemote.right.length >= Node.sizeof); + + Node* left = toDemote.left; + Node* right = toDemote.right; + + size_t weightOfToDemote = weightOf(toDemote), + weightOfLeft = weightOf(left), weightOfRight = weightOf(right); + + while (weightOfLeft > weightOfToDemote || weightOfRight > weightOfToDemote) + { + if (weightOfLeft >= weightOfRight) + { + *parent = left; + parent = &left.right; + + left = *parent; + weightOfLeft = weightOf(left); + } + else + { + *parent = right; + parent = &right.left; + + right = *parent; + weightOfRight = weightOf(right); + } + } + + *parent = toDemote; + toDemote.left = left; + toDemote.right = right; + } + + static struct Node + { + Node* left, right; + size_t length; + + @safe @nogc scope nothrow @hidden: + + void[] recreate() @trusted + { + assert(length > 0); + return (cast(void*)&this)[0 .. length]; + } + } + + size_t weightOf(Node* node) + { + assert(node is null || node.length > 0); + return node is null ? 0 : node.length; + } + + bool fitsAlignment(Node** node, size_t needed, size_t alignedTo) + { + if (node is null || *node is null) + return false; + + assert((*node).length >= Node.sizeof); + assert((*node).left is null || (*node).left.length >= Node.sizeof); + assert((*node).right is null || (*node).right.length >= Node.sizeof); + + if (alignedTo == 0) + return (*node).length >= needed; + + size_t padding = alignedTo - ((cast(size_t)*node) % alignedTo); + if (padding == alignedTo) + padding = 0; + + return needed + padding <= (*node).length; + } + + void[] allocateImpl(size_t size, Node** parent) + { + Node* current = *parent; + assert(current !is null); + assert(current.length >= Node.sizeof); + assert(current.left is null || current.left.length >= Node.sizeof); + assert(current.right is null || current.right.length >= Node.sizeof); + + size_t toAddAlignment = alignedTo - ((cast(size_t) current) % alignedTo); + + if (toAddAlignment == alignedTo) + toAddAlignment = 0; + + assert(current.length >= size + toAddAlignment); + + size_t actualAllocationSize = size; + if (actualAllocationSize < Node.sizeof) + actualAllocationSize = Node.sizeof; + + if (current.length <= actualAllocationSize + toAddAlignment + Node.sizeof + + minimumStoredSize) + { + static if (storeAllocated) + { + allocations.store(current.recreate()); + } + + delete_(parent); + } + else + { + assert(current.length >= actualAllocationSize + toAddAlignment + Node.sizeof); + + static if (storeAllocated) + { + allocations.store(current.recreate()[0 .. actualAllocationSize + toAddAlignment]); + } + + Node* temp = cast(Node*)((cast(size_t) current) + actualAllocationSize + toAddAlignment); + temp.left = current.left; + temp.right = current.right; + temp.length = current.length - (actualAllocationSize + toAddAlignment); + + *parent = temp; + demote(parent); + } + + return current.recreate()[toAddAlignment .. toAddAlignment + actualAllocationSize]; + } +} + +/// +unittest +{ + import phobos.sys.allocators.mapping.malloc; + import phobos.sys.allocators.buffers.region; + + void perform(FT)() + { + FT ft; + assert(!ft.empty); + assert(!ft.isNull); + + ft = FT(); + assert(!ft.empty); + assert(!ft.isNull); + + void[] got1 = ft.allocate(1024); + assert(got1 !is null); + assert(got1.length == 1024); + assert(ft.owns(null) == Ternary.no); + assert(ft.owns(got1) == Ternary.yes); + assert(ft.owns(got1[10 .. 20]) == Ternary.yes); + + void[] got2 = ft.allocate(512); + assert(got2 !is null); + assert(got2.length == 512); + assert(ft.owns(null) == Ternary.no); + assert(ft.owns(got2) == Ternary.yes); + assert(ft.owns(got2[10 .. 20]) == Ternary.yes); + + void[] got3 = ft.allocate(1024); + assert(got3 !is null); + assert(got3.length == 1024); + assert(ft.owns(null) == Ternary.no); + assert(ft.owns(got3) == Ternary.yes); + assert(ft.owns(got3[10 .. 20]) == Ternary.yes); + + bool success = ft.reallocate(got1, 2048); + assert(!success); + assert(got1.length == 1024); + + assert(ft.owns(got1) == Ternary.yes); + success = ft.deallocate(got1); + assert(success); + success = ft.deallocate(got2); + assert(success); + success = ft.deallocate(got3); + assert(success); + + got1 = ft.allocate(512); + assert(got1 !is null); + assert(got1.length == 512); + assert(ft.owns(null) == Ternary.no); + assert(ft.owns(got1) == Ternary.yes); + assert(ft.owns(got1[10 .. 20]) == Ternary.yes); + } + + perform!(FreeTree!(Region!Mallocator, FitsStrategy.FirstFit)); + perform!(FreeTree!(Region!Mallocator, FitsStrategy.NextFit)); + perform!(FreeTree!(Region!Mallocator, FitsStrategy.BestFit)); + perform!(FreeTree!(Region!Mallocator, FitsStrategy.WorstFit)); +} diff --git a/phobos/sys/allocators/buffers/region.d b/phobos/sys/allocators/buffers/region.d new file mode 100644 index 00000000000..64dd196a7b9 --- /dev/null +++ b/phobos/sys/allocators/buffers/region.d @@ -0,0 +1,335 @@ +/** +Fixed region memory allocation strategy. +Can allocate and deallocate by using another memory mapper if one is provided. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.buffers.region; +import phobos.sys.allocators.mapping : GoodAlignment; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + import phobos.sys.allocators.api; + + alias RegionRC = Region!RCAllocator; +} + +export: + +/** +A bump the pointer allocator for a set slice of memory, will automically allocate if required and can guarantee alignment. + +Does not use `TypeInfo`, will not be forwarded on allocation. + +Warning: does not destroy on deallocation. +*/ +struct Region(PoolAllocator = void, size_t DefaultAlignment = GoodAlignment, size_t DefaultSize = 0) +{ +export: + /// + void[] memory; + + static if (!is(PoolAllocator == void)) + { + /// Automatically deallocation of memory using this allocator + PoolAllocator poolAllocator; + } + + /// + size_t alignedTo = DefaultAlignment; + /// + size_t defaultSize = DefaultSize; + + /// + enum NeedsLocking = true; + + private size_t allocated; + +@safe @nogc scope nothrow: + + /// + this(void[] memory, size_t alignedTo = DefaultAlignment, size_t defaultSize = DefaultSize) + { + this.memory = memory; + this.alignedTo = alignedTo; + this.defaultSize = defaultSize; + } + + /// + bool isNull() const + { + static if (!is(PoolAllocator == void)) + { + if (!poolAllocator.isNull) + return false; + else + return memory is null; + } + else + return memory is null; + } + +@trusted: + + static if (!is(PoolAllocator == void)) + { + /// + this(void[] memory, PoolAllocator poolAllocator, + size_t alignedTo = DefaultAlignment, size_t defaultSize = DefaultSize) + { + this.memory = memory; + this.poolAllocator = poolAllocator; + this.alignedTo = alignedTo; + this.defaultSize = defaultSize; + } + } + + /// + this(return scope ref Region other) + { + this.tupleof = other.tupleof; + this.allocated = other.allocated; + other.memory = null; + other = Region.init; + } + + ~this() + { + deallocateAll(); + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + return allocate_(size, ti); + } + + private void[] allocate_(size_t size, TypeInfo ti = null) @system @hidden + { + static if (!is(PoolAllocator == void)) + { + if (memory is null) + { + import phobos.sys.allocators.mapping.vars : PAGESIZE; + + if (defaultSize == 0) + defaultSize = PAGESIZE; + + size_t toAllocateSize = defaultSize; + if (toAllocateSize < size + alignedTo) + toAllocateSize = size + alignedTo; + + memory = poolAllocator.allocate(toAllocateSize, null); + + version (none) + { + import core.stdc.stdio; + + debug printf("allocate requested length %zd, actual length %zd, got pointer %p, got length %zd\n", + size, toAllocateSize, memory.ptr, memory.length); + debug fflush(stdout); + } + } + } + + if (allocated + size > memory.length) + return null; + + void[] toGo = memory[allocated .. $]; + + if (fitsAlignment(toGo, size, alignedTo)) + { + size_t toAddAlignment = alignedTo > 0 ? (alignedTo - ((cast(size_t) toGo.ptr) % alignedTo)) + : 0; + if (toAddAlignment == alignedTo) + toAddAlignment = 0; + + allocated += toAddAlignment + size; + auto toReturn = toGo[toAddAlignment .. toAddAlignment + size]; + if (alignedTo > 0) + assert(cast(size_t) toReturn.ptr % alignedTo == 0); + return toReturn; + } + else + return null; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (memory is null || (array.ptr + array.length !is &memory[allocated]) + || (allocated + (newSize - array.length) > memory.length)) + return false; + else if (newSize < array.length) + { + array = array[0 .. newSize]; + return true; + } + else if (newSize > array.length) + { + size_t extra = newSize - array.length; + allocated += extra; + array = array.ptr[0 .. newSize]; + return true; + } + else + return true; + } + + /// + bool deallocate(scope void[] array) + { + if (array !is null && allocated >= array.length + && array.ptr is memory.ptr + (allocated - array.length)) + { + allocated -= array.length; + return true; + } + + return false; + } + + /// + Ternary owns(scope void[] array) + { + return (memory.ptr <= array.ptr && (array.ptr < memory.ptr + memory.length)) ? Ternary.yes + : Ternary.no; + } + + /// + bool deallocateAll() + { + allocated = 0; + void[] temp = memory; + memory = null; + + static if (!is(PoolAllocator == void)) + { + return temp is null || poolAllocator.deallocate(temp); + } + else + return false; + } + + /// + bool empty() + { + static if (!is(PoolAllocator == void) && __traits(hasMember, PoolAllocator, "empty")) + { + return (memory is null || allocated == memory.length) && poolAllocator.empty; + } + else + return memory is null || allocated == memory.length; + } + + package(phobos.sys.allocators) + { + bool isOnlyOneAllocationOfSize(size_t size) + { + size_t padding = alignedTo - ((cast(size_t) memory.ptr) % alignedTo); + if (padding == alignedTo) + padding = 0; + + return padding + size == allocated; + } + } + +private @hidden: + bool fitsAlignment(void[] into, size_t needed, size_t alignedTo) @trusted + { + if (alignedTo == 0) + return into.length >= needed; + + size_t padding = alignedTo - ((cast(size_t) into.ptr) % alignedTo); + if (padding == alignedTo) + padding = 0; + + return needed + padding <= into.length; + } +} + +/// +unittest +{ + alias R = Region!(); + + R region; + assert(region.empty); + assert(region.isNull); + + void[] rawMemory = new void[64 * 1024]; + region = R(rawMemory); + assert(!region.empty); + assert(!region.isNull); + + void[] got = region.allocate(1024); + assert(got !is null); + assert(got.length == 1024); + assert(region.owns(got) == Ternary.yes); + assert(region.owns(got[10 .. 20]) == Ternary.yes); + + R region2 = region; + region = region2; + + void* rootGot = got.ptr; + size_t alignmentCheck = region.alignedTo - (cast(size_t) region.memory.ptr % region.alignedTo); + if (alignmentCheck == region.alignedTo) + alignmentCheck = 0; + assert(rootGot - alignmentCheck is region.memory.ptr); + + bool success = region.reallocate(got, 2048); + assert(success); + assert(got.length == 2048); + assert(got.ptr is rootGot); + + assert(region.owns(null) == Ternary.no); + assert(region.owns(got) == Ternary.yes); + assert(region.owns(got[10 .. 20]) == Ternary.yes); + + success = region.deallocate(got); + assert(success); +} + +/// +unittest +{ + import phobos.sys.allocators.mapping.malloc; + + alias R = Region!(Mallocator); + + R region; + assert(!region.empty); + assert(!region.isNull); + + region = R(null, Mallocator()); + assert(!region.empty); + assert(!region.isNull); + + void[] got = region.allocate(1024); + assert(got !is null); + assert(got.length == 1024); + assert(region.owns(got) == Ternary.yes); + assert(region.owns(got[10 .. 20]) == Ternary.yes); + + void* rootGot = got.ptr; + size_t alignmentCheck = region.alignedTo - (cast(size_t) region.memory.ptr % region.alignedTo); + if (alignmentCheck == region.alignedTo) + alignmentCheck = 0; + assert(rootGot - alignmentCheck is region.memory.ptr); + + bool success = region.reallocate(got, 2048); + assert(success); + assert(got.length == 2048); + assert(got.ptr is rootGot); + + assert(region.owns(null) == Ternary.no); + assert(region.owns(got) == Ternary.yes); + assert(region.owns(got[10 .. 20]) == Ternary.yes); + + success = region.deallocate(got); + assert(success); +} diff --git a/phobos/sys/allocators/locking.d b/phobos/sys/allocators/locking.d new file mode 100644 index 00000000000..59a3e16986b --- /dev/null +++ b/phobos/sys/allocators/locking.d @@ -0,0 +1,333 @@ +/** +Provides an allocator wrapper that performs locking to make it thread-safe. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole + */ +module phobos.sys.allocators.locking; +import phobos.sys.typecons : Ternary; + +export: + +/** +Adds a lock around all allocator operations to make it thread safe. +*/ +struct AllocatorLocking(PoolAllocator) +{ + /// + PoolAllocator poolAllocator; + + /// + enum NeedsLocking = false; + + private + { + import phobos.sys.internal.mutualexclusion; + + TestTestSetLockInline mutex; + } + +scope @safe @nogc nothrow: + + /// + this(return scope ref AllocatorLocking other) @trusted + { + assert(other.mutex.lock, "Failed to lock mutex"); + scope (exit) + other.mutex.unlock; + + this.poolAllocator = other.poolAllocator; + other.poolAllocator = PoolAllocator.init; + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) + { + mutex.lock; + scope (exit) + mutex.unlock; + + return poolAllocator.allocate(size, ti); + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + mutex.lock; + scope (exit) + mutex.unlock; + + return poolAllocator.reallocate(array, newSize); + } + + /// + bool deallocate(scope void[] array) + { + if (array is null) + return false; + + mutex.lock; + scope (exit) + mutex.unlock; + + return poolAllocator.deallocate(array); + } + + static if (__traits(hasMember, PoolAllocator, "owns")) + { + /// + Ternary owns(scope void[] array) + { + mutex.lock; + scope (exit) + mutex.unlock; + + return poolAllocator.owns(array); + } + } + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + /// + bool deallocateAll() + { + mutex.lock; + scope (exit) + mutex.unlock; + + return poolAllocator.deallocateAll(); + } + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + mutex.lock; + scope (exit) + mutex.unlock; + + return poolAllocator.empty(); + } + } +} + +/** +Hooks allocations and add then remove ranges as allocations/deallocations/reallocations occur. +*/ +struct GCAllocatorLock(PoolAllocator) +{ + import phobos.sys.allocators.storage.allocatedtree; + import phobos.sys.allocators.mapping.malloc; + import phobos.sys.internal.mutualexclusion; + + /// + PoolAllocator poolAllocator; + + private + { + AllocatedTree!() allocatedTree; + TestTestSetLockInline mutex; + } + + /// + enum NeedsLocking = false; + +scope @safe @nogc nothrow: + + /// + this(return scope ref GCAllocatorLock other) @trusted + { + mutex.lock; + scope (exit) + mutex.unlock; + + this.poolAllocator = other.poolAllocator; + other.poolAllocator = PoolAllocator.init; + + this.allocatedTree = other.allocatedTree; + other.allocatedTree = typeof(allocatedTree).init; + } + + ~this() + { + import core.memory : GC; + + mutex.lock; + scope (exit) + mutex.unlock; + + allocatedTree.deallocateAll((void[] array) @trusted { + GC.removeRange(array.ptr); + return true; + }); + } + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + + /// + void[] allocate(size_t size, TypeInfo ti = null) @trusted + { + import core.memory : GC; + + mutex.lock; + scope (exit) + mutex.unlock; + + void[] got = poolAllocator.allocate(size, ti); + + if (got !is null) + { + allocatedTree.store(got); + GC.addRange(got.ptr, got.length, ti); + return got[0 .. size]; + } + + return null; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) @trusted + { + import core.memory : GC; + + mutex.lock; + scope (exit) + mutex.unlock; + + TypeInfo ti; + auto trueArray = allocatedTree.getTrueRegionOfMemory(array, ti); + if (trueArray is null) + return false; + array = trueArray; + + gcdisable; + + allocatedTree.remove(trueArray); + GC.removeRange(trueArray.ptr); + + bool got = poolAllocator.reallocate(array, newSize); + + if (got) + { + array = array[0 .. newSize]; + + allocatedTree.store(array, ti); + GC.addRange(array.ptr, array.length, ti); + } + else + { + const pointerDifference = array.ptr - trueArray.ptr; + const lengthAvailable = trueArray.length - pointerDifference; + + if (lengthAvailable >= newSize) + { + got = true; + array = trueArray[0 .. newSize]; + } + + allocatedTree.store(trueArray, ti); + GC.addRange(trueArray.ptr, trueArray.length, ti); + } + + gcenable; + return got; + } + + /// + bool deallocate(scope void[] array) @trusted + { + import core.memory : GC; + + if (array is null) + return false; + + mutex.lock; + scope (exit) + mutex.unlock; + + auto trueArray = allocatedTree.getTrueRegionOfMemory(array); + if (trueArray is null) + return false; + + assert(trueArray.ptr <= array.ptr); + assert(trueArray.length >= array.length); + + allocatedTree.remove(trueArray); + GC.removeRange(trueArray.ptr); + + const got = poolAllocator.deallocate(trueArray); + + return got; + } + + /// + Ternary owns(scope void[] array) + { + mutex.lock; + scope (exit) + mutex.unlock; + + return allocatedTree.owns(array); + } + + /// + bool deallocateAll() + { + import core.memory : GC; + + mutex.lock; + scope (exit) + mutex.unlock; + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + allocatedTree.deallocateAll((void[] array) @trusted { + GC.removeRange(array.ptr); + return true; + }); + return poolAllocator.deallocateAll(); + } + else + { + allocatedTree.deallocateAll((void[] array) @trusted { + GC.removeRange(array.ptr); + return poolAllocator.deallocate(array); + }); + return true; + } + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + mutex.lock; + scope (exit) + mutex.unlock; + + return poolAllocator.empty(); + } + } +} + +private: + +extern (C) +{ + pragma(mangle, "gc_enable") static void gcenable() nothrow pure @nogc; + pragma(mangle, "gc_disable") static void gcdisable() nothrow pure @nogc; +} diff --git a/phobos/sys/allocators/mapping/malloc.d b/phobos/sys/allocators/mapping/malloc.d new file mode 100644 index 00000000000..23e85383464 --- /dev/null +++ b/phobos/sys/allocators/mapping/malloc.d @@ -0,0 +1,125 @@ +/** +Provides memory mapping via the libc malloc/realloc/free functions. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.mapping.malloc; +import phobos.sys.allocators.api : RCAllocatorInstance; + +export: + +/** +Libc malloc + realloc + free based memory allocator, should be treated as a mapping allocator but can be used as an allocator. + +Does not use `TypeInfo` argument on allocation. + +Warning: Deallocating using this without keeping track of roots will fail. + +Warning: does not destroy on deallocation. +*/ +struct Mallocator +{ +export: + + /// + enum NeedsLocking = false; + + /// + enum isNull = false; + + /// + __gshared RCAllocatorInstance!Mallocator instance; + +@nogc scope pure nothrow @trusted: + + /// + bool empty() + { + return false; + } + + /// + void[] allocate(size_t length, TypeInfo ti = null) + { + // implementation defined behavior == bad + if (length == 0) + return null; + + void* ret = pureMalloc(length); + + version (none) + { + import core.stdc.stdio; + + debug printf("allocate length %zd, got pointer %p\n", length, ret); + debug fflush(stdout); + } + + if (ret is null) + return null; + else + return ret[0 .. length]; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + // implementation defined behavior == bad + if (newSize == 0) + return false; + + void* ret = pureRealloc(array.ptr, newSize); + + version (none) + { + import core.stdc.stdio; + + debug printf("reallocate old length %zd, new length %zd, old pointer %p, new pointer %p\n", + array.length, newSize, array.ptr, ret); + debug fflush(stdout); + } + + if (ret !is null) + { + array = ret[0 .. newSize]; + return true; + } + else + { + return false; + } + } + + /// + bool deallocate(scope void[] data) @trusted + { + version (none) + { + import core.stdc.stdio; + + debug printf("deallocate length %zd, pointer %p\n", data.length, data.ptr); + debug fflush(stdout); + } + + if (data.ptr !is null) + { + pureFree(data.ptr); + return true; + } + else + return false; + } +} + +private: + +// copied from druntime +extern (C) pure @system @nogc nothrow +{ + pragma(mangle, "malloc") void* pureMalloc(size_t); + pragma(mangle, "calloc") void* pureCalloc(size_t nmemb, size_t size); + pragma(mangle, "realloc") void* pureRealloc(void* ptr, size_t size); + pragma(mangle, "free") void pureFree(void* ptr); +} diff --git a/phobos/sys/allocators/mapping/mmap.d b/phobos/sys/allocators/mapping/mmap.d new file mode 100644 index 00000000000..85dc0e685a2 --- /dev/null +++ b/phobos/sys/allocators/mapping/mmap.d @@ -0,0 +1,81 @@ +/** +Provides Posix specific memory mapping via the mmap/munmap functions. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole + */ +module phobos.sys.allocators.mapping.mmap; +import core.sys.posix.sys.mman; + +export: + +version (Posix) +{ + /** + A posix `mmap` based allocator. + + Does not use `TypeInfo` argument on allocation. + + Warning: do not use this as an allocator directly, it can only function as a memory mapper. + + Warning: does not destroy on deallocation. + */ + struct MMap + { + export: + /// + enum NeedsLocking = false; + + /// + enum isNull = false; + + /// + __gshared RCAllocatorInstance!MMap instance; + + @nogc scope pure nothrow @trusted: + + /// + bool empty() + { + return false; + } + + /// + void[] allocate(size_t length, TypeInfo ti = null) + { + if (length == 0) + return null; + + void* ret = (cast(MMAP)&mmap)(null, length, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + + if (ret is MAP_FAILED) + return null; + else + return ret[0 .. length]; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + return false; + } + + /// + bool deallocate(scope void[] array) + { + if (array.length > 0) + { + (cast(MUNMAP)&munmap)(array.ptr, array.length); + return true; + } + else + return false; + } + } + +private: + alias MMAP = void* function(void*, size_t, int, int, int, off_t) pure nothrow @safe @nogc; + alias MUNMAP = void* function(void*, size_t) pure nothrow @safe @nogc; +} diff --git a/phobos/sys/allocators/mapping/package.d b/phobos/sys/allocators/mapping/package.d new file mode 100644 index 00000000000..4ceee5ee343 --- /dev/null +++ b/phobos/sys/allocators/mapping/package.d @@ -0,0 +1,35 @@ +/** +Provides memory mapping for a given platform, along with the default to use. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole + */ +module phobos.sys.allocators.mapping; +/// +public import phobos.sys.allocators.mapping.vars; + +/// +public import phobos.sys.allocators.mapping.malloc; + +version (Windows) +{ + /// + public import phobos.sys.allocators.mapping.virtualalloc; + + /// + alias DefaultMapper = VirtualAllocMapper; +} +else version (Posix) +{ + /// + public import phobos.sys.allocators.mapping.mmap; + + /// + alias DefaultMapper = MMap; +} +else +{ + /// + alias DefaultMapper = Mallocator; +} diff --git a/phobos/sys/allocators/mapping/vars.d b/phobos/sys/allocators/mapping/vars.d new file mode 100644 index 00000000000..ca54251a931 --- /dev/null +++ b/phobos/sys/allocators/mapping/vars.d @@ -0,0 +1,57 @@ +/** + Note: has code from druntime. + + License: Boost + */ +module phobos.sys.allocators.mapping.vars; +export @trusted nothrow @nogc: + +/// Size of the L1 cpu cache line +enum GoodAlignment = 64; + +/// +@property size_t PAGESIZE() pure +{ + return (cast(typeof(&PAGESIZE))&PAGESIZE_get)(); +} + +private +{ + // Bug: https://issues.dlang.org/show_bug.cgi?id=22031 + size_t PAGESIZE_get() @safe + { + if (PAGESIZE_ == 0) + initializeMappingVariables(); + return PAGESIZE_; + } + + size_t PAGESIZE_; +} + +private: + +void initializeMappingVariables() +{ + // COPIED FROM druntime core.thread.types + version (Windows) + { + import core.sys.windows.winbase; + + SYSTEM_INFO info; + GetSystemInfo(&info); + + PAGESIZE_ = info.dwPageSize; + assert(PAGESIZE < int.max); + } + else version (Posix) + { + import core.sys.posix.unistd; + + PAGESIZE_ = cast(size_t) sysconf(_SC_PAGESIZE); + } + else + { + pragma(msg, "Unknown platform, defaulting PAGESIZE in " ~ __MODULE__ ~ " to 64kb"); + PAGESIZE_ = 64 * 1024; + } +} diff --git a/phobos/sys/allocators/mapping/virtualalloc.d b/phobos/sys/allocators/mapping/virtualalloc.d new file mode 100644 index 00000000000..933f100234b --- /dev/null +++ b/phobos/sys/allocators/mapping/virtualalloc.d @@ -0,0 +1,215 @@ +/** +Windows specific memory mappers. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.mapping.virtualalloc; +import phobos.sys.allocators.api : RCAllocatorInstance; + +export: + +version (Windows) +{ + /** + Maps using the Windows `VirtualAlloc` function. + + Does not use `TypeInfo` argument on allocation. + + Note: this is designed for 4k increments! It will do 4k increments regardless of what you want. + + Warning: It cannot reallocate, use HeapAllocMapper instead. + + Warning: does not destroy on deallocation. + */ + struct VirtualAllocMapper + { + export: + /// + enum NeedsLocking = false; + + /// + enum isNull = false; + + /// + __gshared RCAllocatorInstance!VirtualAllocMapper instance; + + @nogc scope pure nothrow @trusted: + + /// + bool empty() + { + return false; + } + + /// + void[] allocate(size_t length, TypeInfo ti = null) + { + enum FourKb = 4 * 1024; + + size_t leftOver; + if ((leftOver = length % FourKb) != 0) + length += FourKb - leftOver; + + void* ret = VirtualAlloc(null, length, MEM_COMMIT, PAGE_READWRITE); + + version (none) + { + import core.stdc.stdio; + + debug printf("allocate length %zd, got pointer %p\n", length, ret); + debug fflush(stdout); + } + + if (ret !is null) + return ret[0 .. length]; + else + return null; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + return false; + } + + /// + bool deallocate(scope void[] array) + { + version (none) + { + import core.stdc.stdio; + + debug printf("deallocate length %zd, pointer %p\n", array.length, array.ptr); + debug fflush(stdout); + } + + return VirtualFree(array.ptr, 0, MEM_RELEASE) != 0; + } + } + + /** + An instance of the Windows `HeapAlloc` allocator. + + Does not use `TypeInfo` argument on allocation. + + Warning: does not destroy on deallocation. + */ + struct HeapAllocMapper + { + export: + /// + enum NeedsLocking = true; + + /// + __gshared RCAllocatorInstance!HeapAllocMapper instance; + + private + { + HANDLE heap; + } + + @nogc scope pure nothrow @trusted: + + /// + bool empty() + { + return false; + } + + /// + this(return scope ref HeapAllocMapper other) + { + import std.algorithm.mutation : move; + + move(other, this); + } + + /// + ~this() + { + deallocateAll(); + } + + /// + bool isNull() const + { + return heap == HANDLE.init; + } + + /// + void[] allocate(size_t length, TypeInfo ti = null) + { + if (heap == HANDLE.init) + heap = HeapCreate(0, 0, 0); + + if (heap != HANDLE.init) + { + void* ret = HeapAlloc(heap, 0, length); + + if (ret !is null) + return ret[0 .. length]; + } + + return null; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + if (heap == HANDLE.init) + return false; + + void* ret = HeapReAlloc(heap, 0, array.ptr, newSize); + + if (ret !is null) + { + array = ret[0 .. newSize]; + return true; + } + else + return false; + } + + /// + bool deallocate(scope void[] data) + { + if (heap == HANDLE.init) + return false; + + return HeapFree(heap, 0, data.ptr) != 0; + } + + /// + bool deallocateAll() + { + if (heap != HANDLE.init) + { + HeapDestroy(heap); + heap = HANDLE.init; + return true; + } + else + return false; + } + } + +private: + import core.sys.windows.winnt : MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, + PAGE_READWRITE, PVOID, LPVOID; + import core.sys.windows.basetsd : SIZE_T; + import core.sys.windows.windef : DWORD, BOOL, HANDLE; + + @nogc pure nothrow @system extern (Windows) + { + PVOID VirtualAlloc(scope PVOID, SIZE_T, DWORD, DWORD); + BOOL VirtualFree(scope PVOID, SIZE_T, DWORD); + + HANDLE HeapCreate(DWORD, SIZE_T, SIZE_T); + BOOL HeapDestroy(scope HANDLE); + LPVOID HeapAlloc(scope HANDLE, DWORD, SIZE_T); + LPVOID HeapReAlloc(scope HANDLE, DWORD, LPVOID, SIZE_T); + BOOL HeapFree(scope HANDLE, DWORD, scope LPVOID); + } +} diff --git a/phobos/sys/allocators/package.d b/phobos/sys/allocators/package.d new file mode 100644 index 00000000000..b4e30d2e4a5 --- /dev/null +++ b/phobos/sys/allocators/package.d @@ -0,0 +1,29 @@ +/** +Memory allocators handle memory mapping (mapping of memory address ranges onto hardware), guaranteeing alignment and optimizing for memory usage patterns. + +A good memory allocator fit for purpose can improve a programs preformance quite significantly. +An ill fitting memory allocator on the other hand can not only slow down a program, but out right result in the program crashing. + +All memory allocators have a memory mapper associated with them, either initially initializing it with a mapping or to allow further mapping as needed. +There are a few memory mappers implemented in this library. The first is malloc to provide a portable mapper that can be used as a fallback. +VirtualAlloc + HeapAlloc (Windows), and mmap (Posix) are also available. A common one not implemented is sbrk(Posix) as it was deprecated in the early 2000's. + +On top of a memory mapper you will typically use either an allocator list with regions, or a buddy list. + +The top most level is quite often some sort of allocation size allocator determiner with some sort of buffer like a coalescing free tree. + +It is highly recommended by the author that when a type owns memory to not pass in a memory allocator but rather use the global allocator. +For types that are designed to be aggregated, these should take a memory allocator but default to the global allocator. +When data structures accept aggregatable types, it should also optionally support a memory allocator to deallocate its memory when removing. + +When allocating, the `TypeInfo` parameter may not be used. +This will be made available to the memory mapper when forwarding calls. + +Be aware that this library will not call destructors for you as part of the allocator itself. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators; +public import phobos.sys.allocators.api; diff --git a/phobos/sys/allocators/predefined.d b/phobos/sys/allocators/predefined.d new file mode 100644 index 00000000000..dcd7590c7a1 --- /dev/null +++ b/phobos/sys/allocators/predefined.d @@ -0,0 +1,153 @@ +/** +A set of predefined but useful memory allocators. + +There are multiple categories of allocators defined here, they are: + +- Mapping: Raw memory mapping, for configuring address ranges to hardware. +- House keeping: used for fixed sized memory allocations (not arrays) and are meant for internals to data structures. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.predefined; +import phobos.sys.allocators.api; +import phobos.sys.allocators.buffers.region; +import phobos.sys.allocators.buffers.freelist; +import phobos.sys.allocators.alternatives.allocatorlist; +import phobos.sys.typecons : Ternary; + +private +{ + alias HouseKeepingAllocatorTest = HouseKeepingAllocator!RCAllocator; +} + +export: + +public import phobos.sys.allocators.mapping : DefaultMapper, GoodAlignment; + +/// An allocator specializing in fixed size allocations that can be deallocated all at once. +alias HouseKeepingAllocator(MappingAllocator = DefaultMapper, size_t AlignedTo = 0) = HouseKeepingFreeList!( + AllocatorList!(MappingAllocator, + (poolAllocator) => Region!(typeof(poolAllocator), AlignedTo)(null, poolAllocator))); + +/// Accumulator of memory regions that can be deallocated all at once, not thread safe. +alias MemoryRegionsAllocator(size_t DefaultSize = 0, MappingAllocator = DefaultMapper) = AllocatorList!( + Region!(MappingAllocator, + GoodAlignment, DefaultSize), () => Region!(MappingAllocator, + GoodAlignment, DefaultSize)()); + +/** +A house keeping allocator that will ensure there are LSB bits available for tags + +Use ``(pointer & TaggedPointerHouseKeepingAllocator.Mask)`` to get tags and ``(pointer & TaggedPointerHouseKeepingAllocator.PointerMask)`` to get the pointer. + +Warning: ensure that the memory returned has been added as a root to any GC you use, if you store GC memory in it. +*/ +template TaggedPointerHouseKeepingAllocator(MappingAllocator = DefaultMapper, int BitsToTag = 1) +{ + static assert(BitsToTag > 0, + "Zero bits to tag is equivalent to packing memory without any alignment. Must be above zero."); + static assert(BitsToTag < size_t.sizeof * 4, + "The number of bits in the tag should be less than half the bits in a pointer..."); + + /// + alias TaggedPointerHouseKeepingAllocator = HouseKeepingAllocator!(MappingAllocator, + 2 ^^ BitsToTag); + + /// Mask to get the bits that contain the tag(s) + enum Mask = (2 ^^ BitsToTag) - 1; + /// Mask to get the bits that contain the pointer + enum PointerMask = ~Mask; +} + +version (D_BetterC) +{ +} +else +{ + /// The garbage collector + struct GCAllocator + { + /// + enum NeedsLocking = false; + + __gshared RCAllocatorInstance!GCAllocator instance; + + export @safe nothrow @nogc: + + this(ref GCAllocator other) + { + } + + ~this() + { + } + + bool isNull() + { + return false; + } + + /// + void[] allocate(size_t amount, TypeInfo ti = null) @trusted + { + auto got = gcmalloc(amount, 0, ti); + + version (none) + { + import core.stdc.stdio; + + debug printf("requested %zd, got %p\n", amount, got); + debug fflush(stdout); + } + + if (got is null) + return null; + + return got[0 .. amount]; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) @trusted + { + void* newPtr = gcrealloc(array.ptr, newSize); + if (newPtr is null) + return false; + + array = newPtr[0 .. newSize]; + return true; + } + + /// + bool deallocate(scope void[] array) @trusted + { + gcfree(array.ptr); + return true; + } + + /// + Ternary owns(scope void[] array) @trusted + { + return gc_sizeOf(array.ptr) > 0 ? Ternary.yes : Ternary.no; + } + } +} + +private: + +version (D_BetterC) +{ +} +else +{ + extern (C) @safe nothrow @nogc + { + pragma(mangle, "gc_malloc") void* gcmalloc(size_t sz, uint ba = 0, + const scope TypeInfo ti = null); + pragma(mangle, "gc_realloc") void* gcrealloc(return scope void* p, + size_t sz, uint ba = 0, const TypeInfo ti = null); + pragma(mangle, "gc_free") void gcfree(void* p); + size_t gc_sizeOf(void* p); + } +} diff --git a/phobos/sys/allocators/stats.d b/phobos/sys/allocators/stats.d new file mode 100644 index 00000000000..4ecac896c18 --- /dev/null +++ b/phobos/sys/allocators/stats.d @@ -0,0 +1,180 @@ +/** +Provides statistics about a given allocator that it wraps. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.stats; +import phobos.sys.typecons : Ternary; +import core.atomic; + +private +{ + import phobos.sys.allocators.api; + + alias Stats = StatsAllocator!RCAllocator; +} + +export: + +/// Tracks some information related to allocation +struct StatsAllocator(PoolAllocator) +{ + /// + PoolAllocator poolAllocator; + + static if (__traits(hasMember, PoolAllocator, "NeedsLocking")) + { + /// + enum NeedsLocking = PoolAllocator.NeedsLocking; + } + else + { + /// + enum NeedsLockin = false; + } + + /// + __gshared RCAllocatorInstance!StatsAllocator instance; + + /// + struct Info + { + /// + size_t callsToOwns, callsToAllocate, callsToAllocateSuccessful, callsToReallocate, + callsToReallocateSuccessful, callsToDeallocation, + callsToDeallocationSuccessful, callsToEmpty; + /// + size_t numberOfReallocationsInPlace; + /// + size_t bytesAllocated, maximumBytesAllocatedOverTime; + } + + private + { + shared(Info) info; + } + +@safe @nogc scope nothrow: + + /// + bool isNull() const + { + return poolAllocator.isNull; + } + +@trusted: + + /// + this(return scope ref StatsAllocator other) + { + this.tupleof = other.tupleof; + other = StatsAllocator.init; + } + + private + { + void updateCAS() + { + size_t bytes, max; + + for (bytes = atomicLoad(info.bytesAllocated), + max = atomicLoad(info.maximumBytesAllocatedOverTime); bytes < max; + cas(&info.maximumBytesAllocatedOverTime, bytes, max)) + { + } + } + } + + /// + Info get() + { + return info; + } + + /// + void[] allocate(size_t length, TypeInfo ti = null) + { + atomicOp!"+="(info.callsToAllocate, 1); + void[] ret = poolAllocator.allocate(length, ti); + + if (ret !is null) + { + atomicOp!"+="(info.callsToAllocateSuccessful, 1); + atomicOp!"+="(info.bytesAllocated, ret.length); + } + + updateCAS; + return ret; + } + + /// + bool deallocate(scope void[] data) + { + atomicOp!"+="(info.callsToDeallocation, 1); + bool ret = poolAllocator.deallocate(data); + + if (ret) + { + atomicOp!"+="(info.callsToDeallocationSuccessful, 1); + atomicOp!"-="(info.bytesAllocated, data.length); + } + + return ret; + } + + /// + bool reallocate(scope ref void[] array, size_t newSize) + { + atomicOp!"+="(info.callsToReallocate, 1); + void[] original = array; + + bool ret = poolAllocator.reallocate(array, newSize); + + if (ret) + atomicOp!"+="(info.callsToReallocateSuccessful, 1); + + if (array.ptr !is null && array.ptr !is original.ptr) + atomicOp!"+="(info.numberOfReallocationsInPlace, 1); + + if (array.ptr !is original.ptr) + { + atomicOp!"-="(info.bytesAllocated, original.length); + atomicOp!"+="(info.bytesAllocated, array.length); + } + + updateCAS; + return ret; + } + + static if (__traits(hasMember, PoolAllocator, "owns")) + { + /// + Ternary owns(scope void[] array) + { + atomicOp!"+="(info.callsToOwns, 1); + return poolAllocator.owns(array); + } + } + + static if (__traits(hasMember, PoolAllocator, "deallocateAll")) + { + /// + bool deallocateAll() + { + atomicStore(info.bytesAllocated, 0); + return poolAllocator.deallocateAll(); + } + } + + static if (__traits(hasMember, PoolAllocator, "empty")) + { + /// + bool empty() + { + atomicOp!"+="(info.callsToEmpty, 1); + return poolAllocator.empty(); + } + } +} diff --git a/phobos/sys/allocators/storage/allocatedlist.d b/phobos/sys/allocators/storage/allocatedlist.d new file mode 100644 index 00000000000..402949f6d9d --- /dev/null +++ b/phobos/sys/allocators/storage/allocatedlist.d @@ -0,0 +1,283 @@ +/* +A list of allocations. + +Prefer this over `AllocatedTree` if you do not need to lookup the true range of a memory. + +See_Also: AllocatedTree + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.storage.allocatedlist; +public import phobos.sys.allocators.predefined : HouseKeepingAllocator; +import phobos.sys.allocators.api; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + alias AL = AllocatedList!(RCAllocator, RCAllocator); +} + +/** +A list of all allocated memory, optionally supports a pool allocator that can be used to automatically deallocate all stored memory. + +Warning: You must remove all memory (i.e. by deallocateAll) prior to destruction or you will get an error. + +Warning: does not destroy on deallocation. +*/ +struct AllocatedList(InternalAllocator = HouseKeepingAllocator!(), PoolAllocator = void) +{ +export: + /// + InternalAllocator internalAllocator; + + static if (!is(PoolAllocator == void)) + { + /// + PoolAllocator poolAllocator; + } + + /// + enum NeedsLocking = true; + + invariant + { + assert(head.next is null || !internalAllocator.isNull); + } + + private + { + Node head; + + static struct Node + { + Node* next; + void[] array; + + bool matches(scope void* other) scope @trusted nothrow @nogc @hidden + { + return array.ptr <= other && (array.ptr + array.length) > other; + } + } + } + +scope @safe @nogc nothrow: + + /// + ~this() + { + static if (!is(PoolAllocator == void)) + { + if (!poolAllocator.isNull) + deallocateAll(); + } + + assert(head.next is null, + "You didn't deallocate all memory before destruction of allocated list."); + } + + /// + bool isNull() const + { + return internalAllocator.isNull; + } + +@trusted: + + /// + this(return scope ref AllocatedList other) + { + this.tupleof = other.tupleof; + other.head.next = null; + other = AllocatedList.init; + } + + static if (!is(PoolAllocator == void)) + { + /// + void deallocateAll() + { + deallocateAll(!poolAllocator.isNull ? &poolAllocator.deallocate : null); + } + } + + /// + void deallocateAll(scope bool delegate(scope void[] array) @trusted nothrow @nogc deallocator) + { + Node* current = head.next; + + while (current !is null) + { + Node* next = current.next; + + if (deallocator !is null) + deallocator(current.array); + internalAllocator.deallocate((cast(void*) current)[0 .. Node.sizeof]); + + current = next; + } + + head.next = null; + } + + /// + void store(scope void[] array) + { + if (array is null) + return; + + Node* previous = &head; + + while (previous.next !is null && previous.next.array.ptr <= array.ptr) + { + Node* current = previous.next; + + if (current.matches(array.ptr)) + { + void* actualStartPtr = current.array.ptr < array.ptr ? current.array.ptr + : array.ptr, actualEndPtr = (current.array.ptr + current.array.length) > ( + array.ptr + array.length) ? (current.array.ptr + current.array.length) : ( + array.ptr + array.length); + size_t actualLength = actualEndPtr - actualStartPtr; + + if (current.array.ptr !is actualStartPtr) + { + previous.next = current.next; + current.array = actualStartPtr[0 .. actualLength]; + + previous = &head; + + while (previous.next !is null && previous.next.array.ptr <= array.ptr) + { + previous = previous.next; + } + + current.next = previous.next; + previous.next = current; + } + else if (current.array.length != actualLength) + { + current.array = actualStartPtr[0 .. actualLength]; + } + + return; + } + + previous = current; + } + + Node* newNode = cast(Node*) internalAllocator.allocate(Node.sizeof); + assert(newNode !is null); + + newNode.next = previous.next; + newNode.array = array; + previous.next = newNode; + } + + /// Caller is responsible for deallocation of memory + void remove(scope void[] array) + { + if (array is null) + return; + + Node* previous = &head; + + while (previous.next !is null && previous.next.array.ptr <= array.ptr) + { + Node* current = previous.next; + + if (current.matches(array.ptr)) + { + previous.next = current.next; + internalAllocator.deallocate((cast(void*) current)[0 .. Node.sizeof]); + return; + } + + previous = current; + } + } + + /// + Ternary owns(scope void[] array) + { + if (array is null) + return Ternary.no; + + Node* previous = &head; + + while (previous.next !is null && previous.next.array.ptr <= array.ptr) + { + Node* current = previous.next; + + if (current.matches(array.ptr)) + return Ternary.yes; + + previous = current; + } + + return Ternary.no; + } + + /// + bool empty() + { + return head.next is null; + } + + /// If memory is stored by us, will return the true region of memory associated with it. + void[] getTrueRegionOfMemory(scope void[] array) + { + if (array is null) + return null; + + Node* previous = &head; + + while (previous.next !is null && previous.next.array.ptr <= array.ptr) + { + Node* current = previous.next; + + if (current.matches(array.ptr)) + return current.array; + + previous = current; + } + + return null; + } +} + +/// +unittest +{ + alias AL = AllocatedList!(); + + AL al; + assert(!al.isNull); + assert(al.empty); + + al = AL(); + assert(!al.isNull); + assert(al.empty); + + void[] someArray = new void[1024]; + al.store(someArray); + assert(!al.empty); + + assert(al.owns(null) == Ternary.no); + assert(al.owns(someArray) == Ternary.yes); + assert(al.owns(someArray[10 .. 20]) == Ternary.yes); + assert(al.getTrueRegionOfMemory(someArray[10 .. 20]) is someArray); + + al.remove(someArray); + assert(al.owns(someArray) == Ternary.no); + assert(al.empty); + + al.store(someArray); + assert(!al.empty); + + int got; + al.deallocateAll((array) { got += array == someArray ? 1 : 0; return true; }); + assert(got == 1); +} diff --git a/phobos/sys/allocators/storage/allocatedtree.d b/phobos/sys/allocators/storage/allocatedtree.d new file mode 100644 index 00000000000..93beb0bf7a3 --- /dev/null +++ b/phobos/sys/allocators/storage/allocatedtree.d @@ -0,0 +1,446 @@ +/* +A list of allocations stored in a tree for fast lookup. + +Prefer this over `AllocatedList` by default. + +See_Also: AllocatedList + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.storage.allocatedtree; +public import phobos.sys.allocators.predefined : HouseKeepingAllocator; +import phobos.sys.allocators.api; +import phobos.sys.internal.attribute : hidden; +import phobos.sys.typecons : Ternary; + +private +{ + alias AT = AllocatedTree!(RCAllocator, RCAllocator); +} + +/** +A tree of all allocated memory, optionally supports a pool allocator that can be used to automatically deallocate all stored memory. + +Warning: You must remove all memory (i.e. by deallocateAll) prior to destruction or you will get an error. + +Warning: does not destroy on deallocation. +*/ +struct AllocatedTree(InternalAllocator = HouseKeepingAllocator!(), PoolAllocator = void) +{ +export: + /// + InternalAllocator internalAllocator; + + static if (!is(PoolAllocator == void)) + { + /// + PoolAllocator poolAllocator; + } + + /// + enum NeedsLocking = true; + + invariant + { + assert(anchor is null || !internalAllocator.isNull); + } + + private + { + Node* anchor; + ulong integritySearchKey; + + static struct Node + { + Node* left, right; + void[] array; + ulong seenIntegrity; + + TypeInfo typeInfo; + + bool matches(scope void* other) scope @trusted nothrow @nogc @hidden + { + return array.ptr <= other && (array.ptr + array.length) > other; + } + } + } + +scope @safe @nogc nothrow: + + /// + ~this() + { + static if (!is(PoolAllocator == void)) + { + if (!poolAllocator.isNull) + deallocateAll(); + } + + assert(anchor is null, + "You didn't deallocate all memory before destruction of allocated list."); + } + + /// + bool isNull() const + { + return internalAllocator.isNull; + } + + /// + this(return scope ref AllocatedTree other) @trusted + { + this.tupleof = other.tupleof; + other = AllocatedTree.init; + } + + static if (!is(PoolAllocator == void)) + { + /// + void deallocateAll() @trusted + { + deallocateAll(!poolAllocator.isNull ? &poolAllocator.deallocate : null); + } + } + + /// + void deallocateAll(scope bool delegate(scope void[] array) @trusted nothrow @nogc deallocator) + { + void handle(Node* current) @trusted + { + if (current.left !is null) + handle(current.left); + if (current.right !is null) + handle(current.right); + + if (deallocator !is null) + deallocator(current.array); + internalAllocator.dispose(current); + } + + if (anchor !is null) + { + handle(anchor); + anchor = null; + } + } + + /// + bool empty() + { + return anchor is null; + } + + /// + Ternary owns(scope void[] array) @trusted + { + if (array is null) + return Ternary.no; + + Node** nodeInParent = findPointerToNodeInParentGivenArray(array); + + if (*nodeInParent is null) + return Ternary.no; + + return (*nodeInParent).matches(array.ptr) ? Ternary.yes : Ternary.no; + } + + /// If memory is stored by us, will return the true region of memory associated with it. + void[] getTrueRegionOfMemory(scope void[] array) @trusted + { + if (array is null) + return null; + + Node** nodeInParent = findPointerToNodeInParentGivenArray(array); + + if (*nodeInParent is null || !(*nodeInParent).matches(array.ptr)) + return null; + + return (*nodeInParent).array; + } + + /// If memory is stored by us, will return the true region of memory associated with it. + void[] getTrueRegionOfMemory(scope void[] array, out TypeInfo ti) @trusted + { + if (array is null) + return null; + + Node** nodeInParent = findPointerToNodeInParentGivenArray(array); + + if (*nodeInParent is null || !(*nodeInParent).matches(array.ptr)) + return null; + + ti = (*nodeInParent).typeInfo; + return (*nodeInParent).array; + } + + /// + void store(scope void[] array, TypeInfo ti = null) @trusted + { + if (array is null) + return; + + Node** parent = findPointerToNodeInParentGivenArray(array); + Node* childOfParent = *parent; + + if (childOfParent !is null && childOfParent.matches(array.ptr)) + { + const startPtrOfInput = array.ptr, lengthOfInput = array.length, + endPtrOfInput = startPtrOfInput + lengthOfInput; + const startPtrOfNode = childOfParent.array.ptr, + lengthOfNode = childOfParent.array.length, + endPtrOfNode = startPtrOfNode + lengthOfNode; + const actualStartPtr = startPtrOfInput > startPtrOfNode ? startPtrOfNode : startPtrOfInput, + actualEndPtr = endPtrOfInput > endPtrOfNode + ? endPtrOfInput : endPtrOfNode, actualLength = actualEndPtr - actualStartPtr; + + if (startPtrOfInput !is startPtrOfNode) + { + removeNodeInParentAndRotate(parent); + array = cast(void[]) actualStartPtr[0 .. actualLength]; + + parent = findPointerToNodeInParentGivenArray(array); + insertNodeIntoParentAndRotate(childOfParent, parent); + } + else if (lengthOfInput > lengthOfNode) + { + childOfParent.array = array; + } + } + else + { + childOfParent = internalAllocator.make!Node; + childOfParent.array = array; + + childOfParent.typeInfo = ti; + insertNodeIntoParentAndRotate(childOfParent, parent); + } + } + + /// Caller is responsible for deallocation of memory + void remove(scope void[] array) @trusted + { + if (array is null) + return; + + Node** parent = findPointerToNodeInParentGivenArray(array); + Node* current = *parent; + + if (current !is null && current.matches(array.ptr)) + { + removeNodeInParentAndRotate(parent); + internalAllocator.deallocate((cast(void*) current)[0 .. Node.sizeof]); + } + } + +private @hidden: + + Node** findPointerToNodeInParentGivenArray(scope void[] array) @trusted + { + const startPtrOfInput = array.ptr; + const endPtrOfInput = startPtrOfInput + array.length; + Node** pointerToParent = &anchor; + + while (*pointerToParent !is null) + { + Node* parent = *pointerToParent; + const startPtrOfParent = parent.array.ptr; + const endPtrOfParent = startPtrOfParent + parent.array.length; + + Node** left = &parent.left, right = &parent.right; + + if (endPtrOfInput <= startPtrOfParent) + pointerToParent = left; + else if (endPtrOfParent <= startPtrOfInput) + pointerToParent = right; + else + break; + } + + return pointerToParent; + } + + void insertNodeIntoParentAndRotate(Node* toInsert, Node** parent) @trusted + { + assert(toInsert !is null); + + const ptrOfInsert = toInsert.array.ptr; + Node** ptrToNodeOnLeftOfParent = &toInsert.left, ptrToNodeOnRightOfParent = &toInsert.right; + + Node* orphenNode = *parent; + *parent = toInsert; + + // handle orphens + while (orphenNode !is null) + { + const ptrOfOrphen = orphenNode.array.ptr; + + if (ptrOfOrphen < ptrOfInsert) + { + *ptrToNodeOnLeftOfParent = orphenNode; + ptrToNodeOnLeftOfParent = &orphenNode.right; + + orphenNode = orphenNode.right; + } + else + { + *ptrToNodeOnRightOfParent = orphenNode; + ptrToNodeOnRightOfParent = &orphenNode.left; + + orphenNode = orphenNode.left; + } + } + + *ptrToNodeOnRightOfParent = null; + *ptrToNodeOnRightOfParent = null; + } + + void removeNodeInParentAndRotate(Node** parent) @trusted + { + Node* leftChild = (*parent).left, rightChild = (*parent).right; + + while (leftChild !is rightChild) + { + const startPtrOfLeft = leftChild !is null ? leftChild.array.ptr : null; + const startPtrOfRight = rightChild !is null ? rightChild.array.ptr : null; + + if (startPtrOfLeft < startPtrOfRight) + { + *parent = rightChild; + parent = &rightChild.left; + + rightChild = rightChild.left; + } + else + { + *parent = leftChild; + parent = &leftChild.right; + + leftChild = leftChild.right; + } + } + + *parent = null; + } + + void verifyIntegrity(int line = __LINE__)() @trusted + { + import core.stdc.stdlib : exit; + + const key = integritySearchKey++; + int reason; + + int perNode(Node* parent) + { + if (parent.seenIntegrity < key) + parent.seenIntegrity = key; + else + return 100; + + if (parent.array.length == 0 || parent.array.ptr is null) + return 150; + + int got; + + if (parent.left !is null) + { + const startChildPtr = parent.left.array.ptr; + const childLength = parent.left.array.length; + const endChildPtr = startChildPtr + childLength; + + if (childLength == 0 || startChildPtr is null) + return 200; + else if (endChildPtr >= parent.array.ptr) + return 250; + + got = perNode(parent.left); + if (got) + return got; + } + + if (parent.right !is null) + { + const startChildPtr = parent.right.array.ptr; + const childLength = parent.right.array.length; + const endParentPtr = parent.array.ptr + parent.array.length; + + if (childLength == 0 || startChildPtr is null) + return 300; + else if (startChildPtr < endParentPtr) + return 350; + + got = perNode(parent.right); + if (got) + return got; + } + + return 0; + } + + if (this.anchor !is null) + reason = perNode(this.anchor); + + if (reason != 0) + debug exit(reason); + } +} + +/// +unittest +{ + alias AT = AllocatedTree!(); + + AT at; + assert(!at.isNull); + assert(at.empty); + + at = AT(); + assert(!at.isNull); + assert(at.empty); + + void[] someArray = new void[1024]; + at.store(someArray); + assert(!at.empty); + assert(at.owns(null) == Ternary.no); + assert(at.owns(someArray) == Ternary.yes); + assert(at.owns(someArray[10 .. 20]) == Ternary.yes); + assert(at.getTrueRegionOfMemory(someArray[10 .. 20]) is someArray); + + void[] someArray2 = new void[512]; + at.store(someArray2); + assert(!at.empty); + assert(at.owns(null) == Ternary.no); + assert(at.owns(someArray2) == Ternary.yes); + assert(at.owns(someArray2[10 .. 20]) == Ternary.yes); + assert(at.getTrueRegionOfMemory(someArray2[10 .. 20]) is someArray2); + + void[] someArray3 = new void[1024]; + at.store(someArray3); + assert(!at.empty); + assert(at.owns(null) == Ternary.no); + assert(at.owns(someArray3) == Ternary.yes); + assert(at.owns(someArray3[10 .. 20]) == Ternary.yes); + assert(at.getTrueRegionOfMemory(someArray3[10 .. 20]) is someArray3); + + at.remove(someArray); + assert(at.owns(someArray) == Ternary.no); + assert(at.owns(someArray2) == Ternary.yes); + assert(at.owns(someArray3) == Ternary.yes); + at.remove(someArray2); + assert(at.owns(someArray) == Ternary.no); + assert(at.owns(someArray2) == Ternary.no); + assert(at.owns(someArray3) == Ternary.yes); + at.remove(someArray3); + assert(at.owns(someArray) == Ternary.no); + assert(at.owns(someArray2) == Ternary.no); + assert(at.owns(someArray3) == Ternary.no); + assert(at.empty); + + at.store(someArray); + assert(!at.empty); + + int got; + at.deallocateAll((array) { got += array == someArray ? 1 : 0; return true; }); + assert(got == 1); +} diff --git a/phobos/sys/allocators/utils.d b/phobos/sys/allocators/utils.d new file mode 100644 index 00000000000..3f5824ddd9f --- /dev/null +++ b/phobos/sys/allocators/utils.d @@ -0,0 +1,47 @@ +/** +Some utility code needed for memory allocators to operate. + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.allocators.utils; + +export @safe nothrow @nogc: + +/// Initialize uninitialized memory to its init state +void fillUninitializedWithInit(T)(scope T[] array...) @trusted +{ + enum InitToZero = __traits(isZeroInit, T); + enum InitToInit = __traits(isScalar, T); + + static if (InitToZero || InitToInit) + { + static if (is(T : void) || InitToZero) + { + alias CastTo = ubyte; + } + else + { + alias CastTo = T; + } + + foreach (ref v; cast(CastTo[]) array) + v = CastTo.init; + } + else + { + immutable initState = cast(immutable(ubyte[])) __traits(initSymbol, T); + assert(initState.length == T.sizeof); + + while (array.length > 0) + { + foreach (i, ref v; (cast(ubyte[]) array)[0 .. initState.length]) + { + v = initState[i]; + } + + array = array[1 .. $]; + } + } +} diff --git a/phobos/sys/internal/attribute.d b/phobos/sys/internal/attribute.d new file mode 100644 index 00000000000..62da888703d --- /dev/null +++ b/phobos/sys/internal/attribute.d @@ -0,0 +1,10 @@ +module phobos.sys.internal.attribute; + +version (DigitalMars) +{ + enum hidden; +} +else +{ + public import core.attribute : hidden; +} diff --git a/phobos/sys/internal/mutualexclusion.d b/phobos/sys/internal/mutualexclusion.d new file mode 100644 index 00000000000..aee4880f48f --- /dev/null +++ b/phobos/sys/internal/mutualexclusion.d @@ -0,0 +1,65 @@ +/* +Mutal exclusion, locks for thread consistency. + +https://en.wikipedia.org/wiki/Eisenberg_%26_McGuire_algorithm +https://en.wikipedia.org/wiki/Szyma%C5%84ski%27s_algorithm + +License: Boost +Authors: Richard (Rikki) Andrew Cattermole +Copyright: 2022-2024 Richard Andrew Cattermole +*/ +module phobos.sys.internal.mutualexclusion; +import core.atomic; +import core.thread; + +export: + +// +struct TestTestSetLockInline +{ + private shared(bool) state; + +export @safe @nogc nothrow: + + // Non-pure will yield the thread lock + void lock() scope @trusted + { + for (;;) + { + while (atomicLoad(state)) + { + Thread.yield(); + } + + if (cas(&state, false, true)) + return; + } + } + +pure: + + // A much more limited lock method, that is pure. + void pureLock() scope + { + for (;;) + { + if (atomicLoad(state)) + atomicFence(); + + if (cas(&state, false, true)) + return; + } + } + + // + bool tryLock() scope + { + return cas(&state, false, true); + } + + // + void unlock() scope + { + atomicStore(state, false); + } +} diff --git a/phobos/sys/typecons.d b/phobos/sys/typecons.d new file mode 100644 index 00000000000..9637bea0c7b --- /dev/null +++ b/phobos/sys/typecons.d @@ -0,0 +1,187 @@ +// Written in the D programming language. + +/** +This module implements a variety of type constructors, i.e., templates +that allow construction of new, useful general-purpose types. + +$(SCRIPT inhibitQuickIndex = 1;) +$(DIVC quickindex, +$(BOOKTABLE, +$(TR $(TH Category) $(TH Symbols)) +$(TR $(TD Types) $(TD + $(LREF Ternary) +)) +)) +*/ +module phobos.sys.typecons; + +/** +Ternary type with three truth values: + +$(UL + $(LI `Ternary.yes` for `true`) + $(LI `Ternary.no` for `false`) + $(LI `Ternary.unknown` as an unknown state) +) + +Also known as trinary, trivalent, or trilean. + +See_Also: + $(HTTP en.wikipedia.org/wiki/Three-valued_logic, + Three Valued Logic on Wikipedia) +*/ +struct Ternary +{ +@safe @nogc nothrow pure: + + private ubyte value = 6; + private static Ternary make(ubyte b) + { + Ternary r = void; + r.value = b; + return r; + } + + /** + The possible states of the `Ternary` + */ + enum no = make(0); + /// ditto + enum yes = make(2); + /// ditto + enum unknown = make(6); + + /** + Construct and assign from a `bool`, receiving `no` for `false` and `yes` + for `true`. + */ + this(bool b) + { + value = b << 1; + } + + /// ditto + void opAssign(bool b) + { + value = b << 1; + } + + /** + Construct a ternary value from another ternary value + */ + this(const Ternary b) + { + value = b.value; + } + + /** + $(TABLE Truth table for logical operations, + $(TR $(TH `a`) $(TH `b`) $(TH `$(TILDE)a`) $(TH `a | b`) $(TH `a & b`) $(TH `a ^ b`)) + $(TR $(TD `no`) $(TD `no`) $(TD `yes`) $(TD `no`) $(TD `no`) $(TD `no`)) + $(TR $(TD `no`) $(TD `yes`) $(TD) $(TD `yes`) $(TD `no`) $(TD `yes`)) + $(TR $(TD `no`) $(TD `unknown`) $(TD) $(TD `unknown`) $(TD `no`) $(TD `unknown`)) + $(TR $(TD `yes`) $(TD `no`) $(TD `no`) $(TD `yes`) $(TD `no`) $(TD `yes`)) + $(TR $(TD `yes`) $(TD `yes`) $(TD) $(TD `yes`) $(TD `yes`) $(TD `no`)) + $(TR $(TD `yes`) $(TD `unknown`) $(TD) $(TD `yes`) $(TD `unknown`) $(TD `unknown`)) + $(TR $(TD `unknown`) $(TD `no`) $(TD `unknown`) $(TD `unknown`) $(TD `no`) $(TD `unknown`)) + $(TR $(TD `unknown`) $(TD `yes`) $(TD) $(TD `yes`) $(TD `unknown`) $(TD `unknown`)) + $(TR $(TD `unknown`) $(TD `unknown`) $(TD) $(TD `unknown`) $(TD `unknown`) $(TD `unknown`)) + ) + */ + Ternary opUnary(string s)() if (s == "~") + { + return make((386 >> value) & 6); + } + + /// ditto + Ternary opBinary(string s)(Ternary rhs) if (s == "|") + { + return make((25_512 >> (value + rhs.value)) & 6); + } + + /// ditto + Ternary opBinary(string s)(Ternary rhs) if (s == "&") + { + return make((26_144 >> (value + rhs.value)) & 6); + } + + /// ditto + Ternary opBinary(string s)(Ternary rhs) if (s == "^") + { + return make((26_504 >> (value + rhs.value)) & 6); + } + + /// ditto + Ternary opBinary(string s)(bool rhs) if (s == "|" || s == "&" || s == "^") + { + return this.opBinary!s(Ternary(rhs)); + } +} + +/// +@safe @nogc nothrow pure unittest +{ + Ternary a; + assert(a == Ternary.unknown); + + assert(~Ternary.yes == Ternary.no); + assert(~Ternary.no == Ternary.yes); + assert(~Ternary.unknown == Ternary.unknown); +} + +@safe @nogc nothrow pure unittest +{ + alias f = Ternary.no, t = Ternary.yes, u = Ternary.unknown; + Ternary[27] truthTableAnd = [ + t, t, t, t, u, u, t, f, f, u, t, u, u, u, u, u, f, f, f, t, f, f, u, f, f, + f, f, + ]; + + Ternary[27] truthTableOr = [ + t, t, t, t, u, t, t, f, t, u, t, t, u, u, u, u, f, u, f, t, t, f, u, u, f, + f, f, + ]; + + Ternary[27] truthTableXor = [ + t, t, f, t, u, u, t, f, t, u, t, u, u, u, u, u, f, u, f, t, t, f, u, u, f, + f, f, + ]; + + for (auto i = 0; i != truthTableAnd.length; i += 3) + { + assert((truthTableAnd[i] & truthTableAnd[i + 1]) == truthTableAnd[i + 2]); + assert((truthTableOr[i] | truthTableOr[i + 1]) == truthTableOr[i + 2]); + assert((truthTableXor[i] ^ truthTableXor[i + 1]) == truthTableXor[i + 2]); + } + + Ternary a; + assert(a == Ternary.unknown); + static assert(!is(typeof({ + if (a) + { + } + }))); + assert(!is(typeof({ auto b = Ternary(3); }))); + a = true; + assert(a == Ternary.yes); + a = false; + assert(a == Ternary.no); + a = Ternary.unknown; + assert(a == Ternary.unknown); + Ternary b; + b = a; + assert(b == a); + assert(~Ternary.yes == Ternary.no); + assert(~Ternary.no == Ternary.yes); + assert(~Ternary.unknown == Ternary.unknown); +} + +@safe @nogc nothrow pure unittest +{ + Ternary a = Ternary(true); + assert(a == Ternary.yes); + assert((a & false) == Ternary.no); + assert((a | false) == Ternary.yes); + assert((a ^ true) == Ternary.no); + assert((a ^ false) == Ternary.yes); +}