1 // Written in the D programming language.
2 /**
3 Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/quantizer.d)
4 */
5 module std.experimental.allocator.building_blocks.quantizer;
6 
7 import std.experimental.allocator.common;
8 
9 /**
10 This allocator sits on top of `ParentAllocator` and quantizes allocation sizes,
11 usually from arbitrary positive numbers to a small set of round numbers (e.g.
12 powers of two, page sizes etc). This technique is commonly used to:
13 
14 $(UL
15 $(LI Preallocate more memory than requested such that later on, when
16 reallocation is needed (e.g. to grow an array), expansion can be done quickly
17 in place. Reallocation to smaller sizes is also fast (in-place) when the new
18 size requested is within the same quantum as the existing size. Code that's
19 reallocation-heavy can therefore benefit from fronting a generic allocator with
20 a `Quantizer`. These advantages are present even if `ParentAllocator` does not
21 support reallocation at all.)
22 $(LI Improve behavior of allocators sensitive to allocation sizes, such as
23 `FreeList` and `FreeTree`. Rounding allocation requests up makes for smaller
24 free lists/trees at the cost of slack memory (internal fragmentation).)
25 )
26 
27 The following methods are forwarded to the parent allocator if present:
28 `allocateAll`, `owns`, `deallocateAll`, `empty`.
29 
30 Preconditions: `roundingFunction` must satisfy three constraints. These are
31 not enforced (save for the use of `assert`) for the sake of efficiency.
32 $(OL
33 $(LI $(D roundingFunction(n) >= n) for all `n` of type `size_t`;)
34 $(LI `roundingFunction` must be monotonically increasing, i.e. $(D
35 roundingFunction(n1) <= roundingFunction(n2)) for all $(D n1 < n2);)
36 $(LI `roundingFunction` must be `nothrow`, `@safe`, `@nogc` and `pure`, i.e.
37 always return the same value for a given `n`.)
38 )
39 */
40 struct Quantizer(ParentAllocator, alias roundingFunction)
41 {
42     import std.traits : hasMember;
43 
44     /**
45     The parent allocator. Depending on whether `ParentAllocator` holds state
46     or not, this is a member variable or an alias for
47     `ParentAllocator.instance`.
48     */
49     static if (stateSize!ParentAllocator)
50     {
51         ParentAllocator parent;
52     }
53     else
54     {
55         alias parent = ParentAllocator.instance;
56         __gshared Quantizer instance;
57     }
58 
59     /**
60     Returns `roundingFunction(n)`.
61     */
62     size_t goodAllocSize(size_t n)
63     {
64         auto result = roundingFunction(n);
65         assert(result >= n);
66         return result;
67     }
68 
69     /**
70     Alignment is identical to that of the parent.
71     */
72     enum alignment = ParentAllocator.alignment;
73 
74     /**
75     Gets a larger buffer `buf` by calling
76     `parent.allocate(goodAllocSize(n))`. If `buf` is `null`, returns
77     `null`. Otherwise, returns $(D buf[0 .. n]).
78     */
79     void[] allocate(size_t n)
80     {
81         auto result = parent.allocate(goodAllocSize(n));
82         return result.ptr ? result.ptr[0 .. n] : null;
83     }
84 
85     static if (hasMember!(ParentAllocator, "allocateZeroed"))
86     package(std) void[] allocateZeroed()(size_t n)
87     {
88         auto result = parent.allocateZeroed(goodAllocSize(n));
89         return result.ptr ? result.ptr[0 .. n] : null;
90     }
91 
92     /**
93     Defined only if `parent.alignedAllocate` exists and works similarly to
94     `allocate` by forwarding to
95     $(D parent.alignedAllocate(goodAllocSize(n), a)).
96     */
97     static if (hasMember!(ParentAllocator, "alignedAllocate"))
98     void[] alignedAllocate(size_t n, uint a)
99     {
100         auto result = parent.alignedAllocate(goodAllocSize(n), a);
101         return result.ptr ? result.ptr[0 .. n] : null;
102     }
103 
104     /**
105     First checks whether there's enough slack memory preallocated for `b`
106     by evaluating $(D b.length + delta <= goodAllocSize(b.length)). If that's
107     the case, expands `b` in place. Otherwise, attempts to use
108     `parent.expand` appropriately if present.
109     */
110     bool expand(ref void[] b, size_t delta)
111     {
112         if (!b || delta == 0) return delta == 0;
113         immutable allocated = goodAllocSize(b.length),
114             needed = b.length + delta,
115             neededAllocation = goodAllocSize(needed);
116         assert(b.length <= allocated);
117         assert(needed <= neededAllocation);
118         assert(allocated <= neededAllocation);
119         // Second test needed because expand must work for null pointers, too.
120         if (allocated == neededAllocation)
121         {
122             // Nice!
123             b = (() @trusted => b.ptr[0 .. needed])();
124             return true;
125         }
126         // Hail Mary
127         static if (hasMember!(ParentAllocator, "expand"))
128         {
129             // Expand to the appropriate quantum
130             auto original = (() @trusted => b.ptr[0 .. allocated])();
131             assert(goodAllocSize(needed) >= allocated);
132             if (!parent.expand(original, neededAllocation - allocated))
133                 return false;
134             // Dial back the size
135             b = (() @trusted => original.ptr[0 .. needed])();
136             return true;
137         }
138         else
139         {
140             return false;
141         }
142     }
143 
144     /**
145     Expands or shrinks allocated block to an allocated size of $(D
146     goodAllocSize(s)). Expansion occurs in place under the conditions required
147     by `expand`. Shrinking occurs in place if $(D goodAllocSize(b.length)
148     == goodAllocSize(s)).
149     */
150     bool reallocate(ref void[] b, size_t s)
151     {
152         if (!b.ptr)
153         {
154             b = allocate(s);
155             return b.length == s;
156         }
157         if (s >= b.length && expand(b, s - b.length)) return true;
158         immutable toAllocate = goodAllocSize(s),
159             allocated = goodAllocSize(b.length);
160         // Are the lengths within the same quantum?
161         if (allocated == toAllocate)
162         {
163             // Reallocation (whether up or down) will be done in place
164             b = b.ptr[0 .. s];
165             return true;
166         }
167         // Defer to parent (or global) with quantized size
168         auto original = b.ptr[0 .. allocated];
169         if (!parent.reallocate(original, toAllocate)) return false;
170         b = original.ptr[0 .. s];
171         return true;
172     }
173 
174     /**
175     Defined only if `ParentAllocator.alignedAllocate` exists. Expansion
176     occurs in place under the conditions required by `expand`. Shrinking
177     occurs in place if $(D goodAllocSize(b.length) == goodAllocSize(s)).
178     */
179     static if (hasMember!(ParentAllocator, "alignedAllocate"))
180     bool alignedReallocate(ref void[] b, size_t s, uint a)
181     {
182         if (!b.ptr)
183         {
184             b = alignedAllocate(s, a);
185             return b.length == s;
186         }
187         if (s >= b.length && b.ptr.alignedAt(a) && expand(b, s - b.length)) return true;
188         immutable toAllocate = goodAllocSize(s),
189             allocated = goodAllocSize(b.length);
190         // Are the lengths within the same quantum?
191         if (allocated == toAllocate && b.ptr.alignedAt(a))
192         {
193             assert(b.ptr); // code above must have caught this
194             // Reallocation (whether up or down) will be done in place
195             b = b.ptr[0 .. s];
196             return true;
197         }
198         // Defer to parent (or global) with quantized size
199         auto original = b.ptr[0 .. allocated];
200         if (!parent.alignedReallocate(original, toAllocate, a)) return false;
201         b = original.ptr[0 .. s];
202         return true;
203     }
204 
205     /**
206     Defined if `ParentAllocator.deallocate` exists and forwards to
207     $(D parent.deallocate(b.ptr[0 .. goodAllocSize(b.length)])).
208     */
209     static if (hasMember!(ParentAllocator, "deallocate"))
210     bool deallocate(void[] b)
211     {
212         if (!b.ptr) return true;
213         return parent.deallocate(b.ptr[0 .. goodAllocSize(b.length)]);
214     }
215 
216     // Forwarding methods
217     mixin(forwardToMember("parent",
218         "allocateAll", "owns", "deallocateAll", "empty"));
219 }
220 
221 ///
222 @system unittest
223 {
224     import std.experimental.allocator.building_blocks.free_tree : FreeTree;
225     import std.experimental.allocator.gc_allocator : GCAllocator;
226 
227     size_t roundUpToMultipleOf(size_t s, uint base)
228     {
229         auto rem = s % base;
230         return rem ? s + base - rem : s;
231     }
232 
233     // Quantize small allocations to a multiple of cache line, large ones to a
234     // multiple of page size
235     alias MyAlloc = Quantizer!(
236         FreeTree!GCAllocator,
237         n => roundUpToMultipleOf(n, n <= 16_384 ? 64 : 4096));
238     MyAlloc alloc;
239     const buf = alloc.allocate(256);
240     assert(buf.ptr);
241 }
242 
243 version (StdUnittest)
244 @system unittest
245 {
246     import std.experimental.allocator.gc_allocator : GCAllocator;
247     alias MyAlloc = Quantizer!(GCAllocator,
248         (size_t n) => n.roundUpToMultipleOf(64));
249     testAllocator!(() => MyAlloc());
250 
251     assert((() pure nothrow @safe @nogc => MyAlloc().goodAllocSize(1))() == 64);
252 
253     auto a = MyAlloc();
254     auto b = a.allocate(42);
255     assert(b.length == 42);
256     // Inplace expand, since goodAllocSize is 64
257     assert((() @safe => a.expand(b, 22))());
258     //assert((() nothrow @safe => a.expand(b, 22))());
259     assert(b.length == 64);
260     // Trigger parent.expand, which may or may not succed
261     //() nothrow @safe { a.expand(b, 1); }();
262     () @safe { a.expand(b, 1); }();
263     assert(a.reallocate(b, 100));
264     assert(b.length == 100);
265     // Ensure deallocate inherits from parent
266     () nothrow @nogc { a.deallocate(b); }();
267 }
268 
269 @system unittest
270 {
271     import std.experimental.allocator.building_blocks.region : Region;
272     import std.experimental.allocator.mallocator : Mallocator;
273     import std.typecons : Ternary;
274 
275     alias Alloc = Quantizer!(Region!(Mallocator),
276             (size_t n) => n.roundUpToMultipleOf(64));
277     auto a = Alloc(Region!Mallocator(1024 * 64));
278     const b = a.allocate(42);
279     assert(b.length == 42);
280     // Check that owns inherits from parent, i.e. Region
281     assert((() pure nothrow @safe @nogc => a.owns(b))() == Ternary.yes);
282     assert((() pure nothrow @safe @nogc => a.owns(null))() == Ternary.no);
283 
284     auto c = a.allocate(42);
285     assert(c.length == 42);
286     assert((() pure nothrow @safe @nogc => a.owns(c))() == Ternary.yes);
287     // Inplace expand, since goodAllocSize is 64
288     assert((() nothrow @safe => a.expand(c, 22))());
289     assert(c.length == 64);
290     // Trigger parent.expand
291     assert((() nothrow @safe => a.expand(c, 1))());
292     assert(c.length == 65);
293     // Check that reallocate inherits from parent
294     assert((() nothrow @nogc => a.reallocate(c, 100))());
295     assert(c.length == 100);
296 }
297 
298 version (StdUnittest)
299 @system unittest
300 {
301     import std.experimental.allocator.building_blocks.region : Region;
302     import std.experimental.allocator.mallocator : Mallocator;
303 
304     alias MyAlloc = Quantizer!(Region!(Mallocator),
305             (size_t n) => n.roundUpToMultipleOf(64));
306     testAllocator!(() => MyAlloc(Region!Mallocator(1024 * 64)));
307 
308     auto a = MyAlloc(Region!Mallocator(1024 * 64));
309     void[] b;
310     assert((() nothrow @nogc => a.alignedReallocate(b, 42, 16))());
311     assert(b.length == 42);
312     assert(alignedAt(&b[0], 16));
313 }
314 
315 version (StdUnittest)
316 @system unittest
317 {
318     import std.experimental.allocator.building_blocks.region : BorrowedRegion;
319     import std.typecons : Ternary;
320 
321     alias MyAlloc = Quantizer!(BorrowedRegion!(),
322         (size_t n) => n.roundUpToMultipleOf(64));
323     testAllocator!(() => MyAlloc(BorrowedRegion!()(new ubyte[1024 * 64])));
324 
325     auto a = MyAlloc(BorrowedRegion!()(new ubyte[1024 * 64]));
326     // Check that empty inherits from parent
327     assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.yes);
328     auto b = a.allocate(42);
329     assert(b.length == 42);
330     assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.no);
331     // Check that deallocateAll inherits from parent
332     assert((() nothrow @nogc => a.deallocateAll())());
333     assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.yes);
334 }