1 /// 2 module stdx.allocator.building_blocks.quantizer; 3 4 import stdx.allocator.common; 5 6 /** 7 This allocator sits on top of $(D ParentAllocator) and quantizes allocation 8 sizes, usually from arbitrary positive numbers to a small set of round numbers 9 (e.g. powers of two, page sizes etc). This technique is commonly used to: 10 11 $(UL 12 $(LI Preallocate more memory than requested such that later on, when 13 reallocation is needed (e.g. to grow an array), expansion can be done quickly 14 in place. Reallocation to smaller sizes is also fast (in-place) when the new 15 size requested is within the same quantum as the existing size. Code that's 16 reallocation-heavy can therefore benefit from fronting a generic allocator 17 with a $(D Quantizer). These advantages are present even if 18 $(D ParentAllocator) does not support reallocation at all.) 19 $(LI Improve behavior of allocators sensitive to allocation sizes, such as $(D 20 FreeList) and $(D FreeTree). Rounding allocation requests up makes for smaller 21 free lists/trees at the cost of slack memory (internal fragmentation).) 22 ) 23 24 The following methods are forwarded to the parent allocator if present: 25 $(D allocateAll), $(D owns), $(D deallocateAll), $(D empty). 26 27 Preconditions: $(D roundingFunction) must satisfy three constraints. These are 28 not enforced (save for the use of $(D assert)) for the sake of efficiency. 29 $(OL 30 $(LI $(D roundingFunction(n) >= n) for all $(D n) of type $(D size_t);) 31 $(LI $(D roundingFunction) must be monotonically increasing, i.e. $(D 32 roundingFunction(n1) <= roundingFunction(n2)) for all $(D n1 < n2);) 33 $(LI $(D roundingFunction) must be $(D pure), i.e. always return the same 34 value for a given $(D n).) 35 ) 36 */ 37 struct Quantizer(ParentAllocator, alias roundingFunction) 38 { 39 40 /** 41 The parent allocator. Depending on whether $(D ParentAllocator) holds state 42 or not, this is a member variable or an alias for 43 `ParentAllocator.instance`. 44 */ 45 static if (stateSize!ParentAllocator) 46 { 47 ParentAllocator parent; 48 } 49 else 50 { 51 alias parent = ParentAllocator.instance; 52 enum Quantizer instance = Quantizer(); 53 } 54 55 /** 56 Returns $(D roundingFunction(n)). 57 */ 58 size_t goodAllocSize(size_t n) 59 { 60 auto result = roundingFunction(n); 61 assert(result >= n); 62 return result; 63 } 64 65 /** 66 Alignment is identical to that of the parent. 67 */ 68 enum alignment = ParentAllocator.alignment; 69 70 /** 71 Gets a larger buffer $(D buf) by calling 72 $(D parent.allocate(goodAllocSize(n))). If $(D buf) is $(D null), returns 73 $(D null). Otherwise, returns $(D buf[0 .. n]). 74 */ 75 void[] allocate(size_t n) 76 { 77 auto result = parent.allocate(goodAllocSize(n)); 78 return result.ptr ? result.ptr[0 .. n] : null; 79 } 80 81 /** 82 Defined only if $(D parent.alignedAllocate) exists and works similarly to 83 $(D allocate) by forwarding to 84 $(D parent.alignedAllocate(goodAllocSize(n), a)). 85 */ 86 static if (__traits(hasMember, ParentAllocator, "alignedAllocate")) 87 void[] alignedAllocate(size_t n, uint) 88 { 89 auto result = parent.alignedAllocate(goodAllocSize(n)); 90 return result.ptr ? result.ptr[0 .. n] : null; 91 } 92 93 /** 94 First checks whether there's enough slack memory preallocated for $(D b) 95 by evaluating $(D b.length + delta <= goodAllocSize(b.length)). If that's 96 the case, expands $(D b) in place. Otherwise, attempts to use 97 $(D parent.expand) appropriately if present. 98 */ 99 bool expand(ref void[] b, size_t delta) 100 { 101 if (!b.ptr) return delta == 0; 102 immutable allocated = goodAllocSize(b.length), 103 needed = b.length + delta, 104 neededAllocation = goodAllocSize(needed); 105 assert(b.length <= allocated); 106 assert(needed <= neededAllocation); 107 assert(allocated <= neededAllocation); 108 // Second test needed because expand must work for null pointers, too. 109 if (allocated == neededAllocation) 110 { 111 // Nice! 112 b = b.ptr[0 .. needed]; 113 return true; 114 } 115 // Hail Mary 116 static if (__traits(hasMember, ParentAllocator, "expand")) 117 { 118 // Expand to the appropriate quantum 119 auto original = b.ptr[0 .. allocated]; 120 assert(goodAllocSize(needed) >= allocated); 121 if (!parent.expand(original, neededAllocation - allocated)) 122 return false; 123 // Dial back the size 124 b = original.ptr[0 .. needed]; 125 return true; 126 } 127 else 128 { 129 return false; 130 } 131 } 132 133 /** 134 Expands or shrinks allocated block to an allocated size of $(D 135 goodAllocSize(s)). Expansion occurs in place under the conditions required 136 by $(D expand). Shrinking occurs in place if $(D goodAllocSize(b.length) 137 == goodAllocSize(s)). 138 */ 139 bool reallocate(ref void[] b, size_t s) 140 { 141 if (!b.ptr) 142 { 143 b = allocate(s); 144 return b.length == s; 145 } 146 if (s >= b.length && expand(b, s - b.length)) return true; 147 immutable toAllocate = goodAllocSize(s), 148 allocated = goodAllocSize(b.length); 149 // Are the lengths within the same quantum? 150 if (allocated == toAllocate) 151 { 152 // Reallocation (whether up or down) will be done in place 153 b = b.ptr[0 .. s]; 154 return true; 155 } 156 // Defer to parent (or global) with quantized size 157 auto original = b.ptr[0 .. allocated]; 158 if (!parent.reallocate(original, toAllocate)) return false; 159 b = original.ptr[0 .. s]; 160 return true; 161 } 162 163 /** 164 Defined only if $(D ParentAllocator.alignedAllocate) exists. Expansion 165 occurs in place under the conditions required by $(D expand). Shrinking 166 occurs in place if $(D goodAllocSize(b.length) == goodAllocSize(s)). 167 */ 168 static if (__traits(hasMember, ParentAllocator, "alignedAllocate")) 169 bool alignedReallocate(ref void[] b, size_t s, uint a) 170 { 171 if (!b.ptr) 172 { 173 b = alignedAllocate(s); 174 return b.length == s; 175 } 176 if (s >= b.length && expand(b, s - b.length)) return true; 177 immutable toAllocate = goodAllocSize(s), 178 allocated = goodAllocSize(b.length); 179 // Are the lengths within the same quantum? 180 if (allocated == toAllocate) 181 { 182 assert(b.ptr); // code above must have caught this 183 // Reallocation (whether up or down) will be done in place 184 b = b.ptr[0 .. s]; 185 return true; 186 } 187 // Defer to parent (or global) with quantized size 188 auto original = b.ptr[0 .. allocated]; 189 if (!parent.alignedReallocate(original, toAllocate, a)) return false; 190 b = original.ptr[0 .. s]; 191 return true; 192 } 193 194 /** 195 Defined if $(D ParentAllocator.deallocate) exists and forwards to 196 $(D parent.deallocate(b.ptr[0 .. goodAllocSize(b.length)])). 197 */ 198 static if (__traits(hasMember, ParentAllocator, "deallocate")) 199 bool deallocate(void[] b) 200 { 201 if (!b.ptr) return true; 202 return parent.deallocate(b.ptr[0 .. goodAllocSize(b.length)]); 203 } 204 205 // Forwarding methods 206 mixin(forwardToMember("parent", 207 "allocateAll", "owns", "deallocateAll", "empty")); 208 } 209 210 /// 211 @system unittest 212 { 213 import stdx.allocator.building_blocks.free_tree : FreeTree; 214 import stdx.allocator.gc_allocator : GCAllocator; 215 216 size_t roundUpToMultipleOf(size_t s, uint base) 217 { 218 auto rem = s % base; 219 return rem ? s + base - rem : s; 220 } 221 222 // Quantize small allocations to a multiple of cache line, large ones to a 223 // multiple of page size 224 alias MyAlloc = Quantizer!( 225 FreeTree!GCAllocator, 226 n => roundUpToMultipleOf(n, n <= 16_384 ? 64 : 4096)); 227 MyAlloc alloc; 228 const buf = alloc.allocate(256); 229 assert(buf.ptr); 230 } 231 232 @system unittest 233 { 234 import stdx.allocator.gc_allocator : GCAllocator; 235 alias MyAlloc = Quantizer!(GCAllocator, 236 (size_t n) => n.roundUpToMultipleOf(64)); 237 testAllocator!(() => MyAlloc()); 238 }