| // Written in the D programming language. |
| /** |
| Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/quantizer.d) |
| */ |
| module std.experimental.allocator.building_blocks.quantizer; |
| |
| import std.experimental.allocator.common; |
| |
| /** |
| This allocator sits on top of `ParentAllocator` and quantizes allocation sizes, |
| usually from arbitrary positive numbers to a small set of round numbers (e.g. |
| powers of two, page sizes etc). This technique is commonly used to: |
| |
| $(UL |
| $(LI Preallocate more memory than requested such that later on, when |
| reallocation is needed (e.g. to grow an array), expansion can be done quickly |
| in place. Reallocation to smaller sizes is also fast (in-place) when the new |
| size requested is within the same quantum as the existing size. Code that's |
| reallocation-heavy can therefore benefit from fronting a generic allocator with |
| a `Quantizer`. These advantages are present even if `ParentAllocator` does not |
| support reallocation at all.) |
| $(LI Improve behavior of allocators sensitive to allocation sizes, such as |
| `FreeList` and `FreeTree`. Rounding allocation requests up makes for smaller |
| free lists/trees at the cost of slack memory (internal fragmentation).) |
| ) |
| |
| The following methods are forwarded to the parent allocator if present: |
| `allocateAll`, `owns`, `deallocateAll`, `empty`. |
| |
| Preconditions: `roundingFunction` must satisfy three constraints. These are |
| not enforced (save for the use of `assert`) for the sake of efficiency. |
| $(OL |
| $(LI $(D roundingFunction(n) >= n) for all `n` of type `size_t`;) |
| $(LI `roundingFunction` must be monotonically increasing, i.e. $(D |
| roundingFunction(n1) <= roundingFunction(n2)) for all $(D n1 < n2);) |
| $(LI `roundingFunction` must be `nothrow`, `@safe`, `@nogc` and `pure`, i.e. |
| always return the same value for a given `n`.) |
| ) |
| */ |
| struct Quantizer(ParentAllocator, alias roundingFunction) |
| { |
| import std.traits : hasMember; |
| |
| /** |
| The parent allocator. Depending on whether `ParentAllocator` holds state |
| or not, this is a member variable or an alias for |
| `ParentAllocator.instance`. |
| */ |
| static if (stateSize!ParentAllocator) |
| { |
| ParentAllocator parent; |
| } |
| else |
| { |
| alias parent = ParentAllocator.instance; |
| __gshared Quantizer instance; |
| } |
| |
| /** |
| Returns `roundingFunction(n)`. |
| */ |
| size_t goodAllocSize(size_t n) |
| { |
| auto result = roundingFunction(n); |
| assert(result >= n); |
| return result; |
| } |
| |
| /** |
| Alignment is identical to that of the parent. |
| */ |
| enum alignment = ParentAllocator.alignment; |
| |
| /** |
| Gets a larger buffer `buf` by calling |
| `parent.allocate(goodAllocSize(n))`. If `buf` is `null`, returns |
| `null`. Otherwise, returns $(D buf[0 .. n]). |
| */ |
| void[] allocate(size_t n) |
| { |
| auto result = parent.allocate(goodAllocSize(n)); |
| return result.ptr ? result.ptr[0 .. n] : null; |
| } |
| |
| static if (hasMember!(ParentAllocator, "allocateZeroed")) |
| package(std) void[] allocateZeroed()(size_t n) |
| { |
| auto result = parent.allocateZeroed(goodAllocSize(n)); |
| return result.ptr ? result.ptr[0 .. n] : null; |
| } |
| |
| /** |
| Defined only if `parent.alignedAllocate` exists and works similarly to |
| `allocate` by forwarding to |
| $(D parent.alignedAllocate(goodAllocSize(n), a)). |
| */ |
| static if (hasMember!(ParentAllocator, "alignedAllocate")) |
| void[] alignedAllocate(size_t n, uint a) |
| { |
| auto result = parent.alignedAllocate(goodAllocSize(n), a); |
| return result.ptr ? result.ptr[0 .. n] : null; |
| } |
| |
| /** |
| First checks whether there's enough slack memory preallocated for `b` |
| by evaluating $(D b.length + delta <= goodAllocSize(b.length)). If that's |
| the case, expands `b` in place. Otherwise, attempts to use |
| `parent.expand` appropriately if present. |
| */ |
| bool expand(ref void[] b, size_t delta) |
| { |
| if (!b || delta == 0) return delta == 0; |
| immutable allocated = goodAllocSize(b.length), |
| needed = b.length + delta, |
| neededAllocation = goodAllocSize(needed); |
| assert(b.length <= allocated); |
| assert(needed <= neededAllocation); |
| assert(allocated <= neededAllocation); |
| // Second test needed because expand must work for null pointers, too. |
| if (allocated == neededAllocation) |
| { |
| // Nice! |
| b = (() @trusted => b.ptr[0 .. needed])(); |
| return true; |
| } |
| // Hail Mary |
| static if (hasMember!(ParentAllocator, "expand")) |
| { |
| // Expand to the appropriate quantum |
| auto original = (() @trusted => b.ptr[0 .. allocated])(); |
| assert(goodAllocSize(needed) >= allocated); |
| if (!parent.expand(original, neededAllocation - allocated)) |
| return false; |
| // Dial back the size |
| b = (() @trusted => original.ptr[0 .. needed])(); |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| /** |
| Expands or shrinks allocated block to an allocated size of $(D |
| goodAllocSize(s)). Expansion occurs in place under the conditions required |
| by `expand`. Shrinking occurs in place if $(D goodAllocSize(b.length) |
| == goodAllocSize(s)). |
| */ |
| bool reallocate(ref void[] b, size_t s) |
| { |
| if (!b.ptr) |
| { |
| b = allocate(s); |
| return b.length == s; |
| } |
| if (s >= b.length && expand(b, s - b.length)) return true; |
| immutable toAllocate = goodAllocSize(s), |
| allocated = goodAllocSize(b.length); |
| // Are the lengths within the same quantum? |
| if (allocated == toAllocate) |
| { |
| // Reallocation (whether up or down) will be done in place |
| b = b.ptr[0 .. s]; |
| return true; |
| } |
| // Defer to parent (or global) with quantized size |
| auto original = b.ptr[0 .. allocated]; |
| if (!parent.reallocate(original, toAllocate)) return false; |
| b = original.ptr[0 .. s]; |
| return true; |
| } |
| |
| /** |
| Defined only if `ParentAllocator.alignedAllocate` exists. Expansion |
| occurs in place under the conditions required by `expand`. Shrinking |
| occurs in place if $(D goodAllocSize(b.length) == goodAllocSize(s)). |
| */ |
| static if (hasMember!(ParentAllocator, "alignedAllocate")) |
| bool alignedReallocate(ref void[] b, size_t s, uint a) |
| { |
| if (!b.ptr) |
| { |
| b = alignedAllocate(s, a); |
| return b.length == s; |
| } |
| if (s >= b.length && b.ptr.alignedAt(a) && expand(b, s - b.length)) return true; |
| immutable toAllocate = goodAllocSize(s), |
| allocated = goodAllocSize(b.length); |
| // Are the lengths within the same quantum? |
| if (allocated == toAllocate && b.ptr.alignedAt(a)) |
| { |
| assert(b.ptr); // code above must have caught this |
| // Reallocation (whether up or down) will be done in place |
| b = b.ptr[0 .. s]; |
| return true; |
| } |
| // Defer to parent (or global) with quantized size |
| auto original = b.ptr[0 .. allocated]; |
| if (!parent.alignedReallocate(original, toAllocate, a)) return false; |
| b = original.ptr[0 .. s]; |
| return true; |
| } |
| |
| /** |
| Defined if `ParentAllocator.deallocate` exists and forwards to |
| $(D parent.deallocate(b.ptr[0 .. goodAllocSize(b.length)])). |
| */ |
| static if (hasMember!(ParentAllocator, "deallocate")) |
| bool deallocate(void[] b) |
| { |
| if (!b.ptr) return true; |
| return parent.deallocate(b.ptr[0 .. goodAllocSize(b.length)]); |
| } |
| |
| // Forwarding methods |
| mixin(forwardToMember("parent", |
| "allocateAll", "owns", "deallocateAll", "empty")); |
| } |
| |
| /// |
| @system unittest |
| { |
| import std.experimental.allocator.building_blocks.free_tree : FreeTree; |
| import std.experimental.allocator.gc_allocator : GCAllocator; |
| |
| size_t roundUpToMultipleOf(size_t s, uint base) |
| { |
| auto rem = s % base; |
| return rem ? s + base - rem : s; |
| } |
| |
| // Quantize small allocations to a multiple of cache line, large ones to a |
| // multiple of page size |
| alias MyAlloc = Quantizer!( |
| FreeTree!GCAllocator, |
| n => roundUpToMultipleOf(n, n <= 16_384 ? 64 : 4096)); |
| MyAlloc alloc; |
| const buf = alloc.allocate(256); |
| assert(buf.ptr); |
| } |
| |
| version (StdUnittest) |
| @system unittest |
| { |
| import std.experimental.allocator.gc_allocator : GCAllocator; |
| alias MyAlloc = Quantizer!(GCAllocator, |
| (size_t n) => n.roundUpToMultipleOf(64)); |
| testAllocator!(() => MyAlloc()); |
| |
| assert((() pure nothrow @safe @nogc => MyAlloc().goodAllocSize(1))() == 64); |
| |
| auto a = MyAlloc(); |
| auto b = a.allocate(42); |
| assert(b.length == 42); |
| // Inplace expand, since goodAllocSize is 64 |
| assert((() @safe => a.expand(b, 22))()); |
| //assert((() nothrow @safe => a.expand(b, 22))()); |
| assert(b.length == 64); |
| // Trigger parent.expand, which may or may not succed |
| //() nothrow @safe { a.expand(b, 1); }(); |
| () @safe { a.expand(b, 1); }(); |
| assert(a.reallocate(b, 100)); |
| assert(b.length == 100); |
| // Ensure deallocate inherits from parent |
| () nothrow @nogc { a.deallocate(b); }(); |
| } |
| |
| @system unittest |
| { |
| import std.experimental.allocator.building_blocks.region : Region; |
| import std.experimental.allocator.mallocator : Mallocator; |
| import std.typecons : Ternary; |
| |
| alias Alloc = Quantizer!(Region!(Mallocator), |
| (size_t n) => n.roundUpToMultipleOf(64)); |
| auto a = Alloc(Region!Mallocator(1024 * 64)); |
| const b = a.allocate(42); |
| assert(b.length == 42); |
| // Check that owns inherits from parent, i.e. Region |
| assert((() pure nothrow @safe @nogc => a.owns(b))() == Ternary.yes); |
| assert((() pure nothrow @safe @nogc => a.owns(null))() == Ternary.no); |
| |
| auto c = a.allocate(42); |
| assert(c.length == 42); |
| assert((() pure nothrow @safe @nogc => a.owns(c))() == Ternary.yes); |
| // Inplace expand, since goodAllocSize is 64 |
| assert((() nothrow @safe => a.expand(c, 22))()); |
| assert(c.length == 64); |
| // Trigger parent.expand |
| assert((() nothrow @safe => a.expand(c, 1))()); |
| assert(c.length == 65); |
| // Check that reallocate inherits from parent |
| assert((() nothrow @nogc => a.reallocate(c, 100))()); |
| assert(c.length == 100); |
| } |
| |
| version (StdUnittest) |
| @system unittest |
| { |
| import std.experimental.allocator.building_blocks.region : Region; |
| import std.experimental.allocator.mallocator : Mallocator; |
| |
| alias MyAlloc = Quantizer!(Region!(Mallocator), |
| (size_t n) => n.roundUpToMultipleOf(64)); |
| testAllocator!(() => MyAlloc(Region!Mallocator(1024 * 64))); |
| |
| auto a = MyAlloc(Region!Mallocator(1024 * 64)); |
| void[] b; |
| assert((() nothrow @nogc => a.alignedReallocate(b, 42, 16))()); |
| assert(b.length == 42); |
| assert(alignedAt(&b[0], 16)); |
| } |
| |
| version (StdUnittest) |
| @system unittest |
| { |
| import std.experimental.allocator.building_blocks.region : BorrowedRegion; |
| import std.typecons : Ternary; |
| |
| alias MyAlloc = Quantizer!(BorrowedRegion!(), |
| (size_t n) => n.roundUpToMultipleOf(64)); |
| testAllocator!(() => MyAlloc(BorrowedRegion!()(new ubyte[1024 * 64]))); |
| |
| auto a = MyAlloc(BorrowedRegion!()(new ubyte[1024 * 64])); |
| // Check that empty inherits from parent |
| assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.yes); |
| auto b = a.allocate(42); |
| assert(b.length == 42); |
| assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.no); |
| // Check that deallocateAll inherits from parent |
| assert((() nothrow @nogc => a.deallocateAll())()); |
| assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.yes); |
| } |