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 }