| /// |
| module std.experimental.allocator.building_blocks.quantizer; |
| |
| import std.experimental.allocator.common; |
| |
| /** |
| This allocator sits on top of $(D 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 $(D Quantizer). These advantages are present even if |
| $(D ParentAllocator) does not support reallocation at all.) |
| $(LI Improve behavior of allocators sensitive to allocation sizes, such as $(D |
| FreeList) and $(D 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: |
| $(D allocateAll), $(D owns), $(D deallocateAll), $(D empty). |
| |
| Preconditions: $(D roundingFunction) must satisfy three constraints. These are |
| not enforced (save for the use of $(D assert)) for the sake of efficiency. |
| $(OL |
| $(LI $(D roundingFunction(n) >= n) for all $(D n) of type $(D size_t);) |
| $(LI $(D roundingFunction) must be monotonically increasing, i.e. $(D |
| roundingFunction(n1) <= roundingFunction(n2)) for all $(D n1 < n2);) |
| $(LI $(D roundingFunction) must be $(D pure), i.e. always return the same |
| value for a given $(D n).) |
| ) |
| */ |
| struct Quantizer(ParentAllocator, alias roundingFunction) |
| { |
| import std.traits : hasMember; |
| |
| /** |
| The parent allocator. Depending on whether $(D 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; |
| static __gshared Quantizer instance; |
| } |
| |
| /** |
| Returns $(D 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 $(D buf) by calling |
| $(D parent.allocate(goodAllocSize(n))). If $(D buf) is $(D null), returns |
| $(D 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; |
| } |
| |
| /** |
| Defined only if $(D parent.alignedAllocate) exists and works similarly to |
| $(D allocate) by forwarding to |
| $(D parent.alignedAllocate(goodAllocSize(n), a)). |
| */ |
| static if (hasMember!(ParentAllocator, "alignedAllocate")) |
| void[] alignedAllocate(size_t n, uint) |
| { |
| auto result = parent.alignedAllocate(goodAllocSize(n)); |
| return result.ptr ? result.ptr[0 .. n] : null; |
| } |
| |
| /** |
| First checks whether there's enough slack memory preallocated for $(D b) |
| by evaluating $(D b.length + delta <= goodAllocSize(b.length)). If that's |
| the case, expands $(D b) in place. Otherwise, attempts to use |
| $(D parent.expand) appropriately if present. |
| */ |
| bool expand(ref void[] b, size_t delta) |
| { |
| if (!b.ptr) 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 = b.ptr[0 .. needed]; |
| return true; |
| } |
| // Hail Mary |
| static if (hasMember!(ParentAllocator, "expand")) |
| { |
| // Expand to the appropriate quantum |
| auto original = b.ptr[0 .. allocated]; |
| assert(goodAllocSize(needed) >= allocated); |
| if (!parent.expand(original, neededAllocation - allocated)) |
| return false; |
| // Dial back the size |
| b = 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 $(D 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 $(D ParentAllocator.alignedAllocate) exists. Expansion |
| occurs in place under the conditions required by $(D 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); |
| 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) |
| { |
| 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 $(D 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.common : roundUpToMultipleOf; |
| import std.experimental.allocator.gc_allocator : GCAllocator; |
| |
| // Quantize small allocations to a multiple of cache line, large ones to a |
| // multiple of page size |
| alias MyAlloc = Quantizer!( |
| FreeTree!GCAllocator, |
| n => n.roundUpToMultipleOf(n <= 16_384 ? 64 : 4096)); |
| MyAlloc alloc; |
| const buf = alloc.allocate(256); |
| assert(buf.ptr); |
| } |
| |
| @system unittest |
| { |
| import std.experimental.allocator.gc_allocator : GCAllocator; |
| alias MyAlloc = Quantizer!(GCAllocator, |
| (size_t n) => n.roundUpToMultipleOf(64)); |
| testAllocator!(() => MyAlloc()); |
| } |