1 ///
2 module stdx.allocator.building_blocks.free_tree;
3 
4 import stdx.allocator.common;
5 
6 //debug = std_experimental_allocator_free_tree;
7 
8 /**
9 
10 The Free Tree allocator, stackable on top of any other allocator, bears
11 similarity with the free list allocator. Instead of a singly-linked list of
12 previously freed blocks, it maintains a binary search tree. This allows the
13 Free Tree allocator to manage blocks of arbitrary lengths and search them
14 efficiently.
15 
16 Common uses of $(D FreeTree) include:
17 
18 $(UL
19 $(LI Adding $(D deallocate) capability to an allocator that lacks it (such as simple regions).)
20 $(LI Getting the benefits of multiple adaptable freelists that do not need to
21 be tuned for one specific size but insted automatically adapts itself to
22 frequently used sizes.)
23 )
24 
25 The free tree has special handling of duplicates (a singly-linked list per
26 node) in anticipation of large number of duplicates. Allocation time from the
27 free tree is expected to be $(BIGOH log n) where $(D n) is the number of
28 distinct sizes (not total nodes) kept in the free tree.
29 
30 Allocation requests first search the tree for a buffer of suitable size
31 deallocated in the past. If a match is found, the node is removed from the tree
32 and the memory is returned. Otherwise, the allocation is directed to $(D
33 ParentAllocator). If at this point $(D ParentAllocator) also fails to allocate,
34 $(D FreeTree) frees everything and then tries the parent allocator again.
35 
36 Upon deallocation, the deallocated block is inserted in the internally
37 maintained free tree (not returned to the parent). The free tree is not kept
38 balanced. Instead, it has a last-in-first-out flavor because newly inserted
39 blocks are rotated to the root of the tree. That way allocations are cache
40 friendly and also frequently used sizes are more likely to be found quickly,
41 whereas seldom used sizes migrate to the leaves of the tree.
42 
43 $(D FreeTree) rounds up small allocations to at least $(D 4 * size_t.sizeof),
44 which on 64-bit system is one cache line size. If very small objects need to
45 be efficiently allocated, the $(D FreeTree) should be fronted with an
46 appropriate small object allocator.
47 
48 The following methods are defined if $(D ParentAllocator) defines them, and forward to it: $(D allocateAll), $(D expand), $(D owns), $(D reallocate).
49 */
50 struct FreeTree(ParentAllocator)
51 {
52     static assert(ParentAllocator.alignment % size_t.alignof == 0,
53         "FreeTree must be on top of a word-aligned allocator");
54 
55     import mir.utility : min, max;
56 
57     // State
58     static if (stateSize!ParentAllocator) private ParentAllocator parent;
59     else private alias parent = ParentAllocator.instance;
60     private Node* root; // that's the entire added state
61 
62     private struct Node
63     {
64         Node*[2] kid;
65         Node* sibling;
66         size_t size;
67         ref Node* left() { return kid[0]; }
68         ref Node* right() { return kid[1]; }
69     }
70 
71     // Removes "which" from the tree, returns the memory it occupied
72     private void[] remove(ref Node* which)
73     {
74         assert(which);
75         assert(!which.sibling);
76         auto result = (cast(ubyte*) which)[0 .. which.size];
77         if (!which.right) which = which.left;
78         else if (!which.left) which = which.right;
79         else
80         {
81             // result has two kids
82             static bool toggler;
83             // Crude randomization: alternate left/right choices
84             toggler = !toggler;
85             auto newRoot = which.kid[toggler], orphan = which.kid[!toggler];
86             which = newRoot;
87             for (Node* n = void; (n = newRoot.kid[!toggler]) !is null; )
88             {
89                 newRoot = n;
90             }
91             newRoot.kid[!toggler] = orphan;
92         }
93         return result;
94     }
95 
96     private void[] findAndRemove(ref Node* n, size_t s)
97     {
98         if (!n) return null;
99         if (s == n.size)
100         {
101             if (auto sis = n.sibling)
102             {
103                 // Nice, give away one from the freelist
104                 auto result = (cast(ubyte*) sis)[0 .. sis.size];
105                 n.sibling = sis.sibling;
106                 return result;
107             }
108             return remove(n);
109         }
110         return findAndRemove(n.kid[s > n.size], s);
111     }
112 
113     debug(std_experimental_allocator_free_tree)
114     private void dump()()
115     {
116         import std.stdio : writef, writefln, writeln;
117         writeln(typeof(this).stringof, "@", &this, " {");
118         scope(exit) writeln("}");
119 
120         if (!root) return;
121 
122         static void recurse(Node* n, uint indent = 4)
123         {
124             if (!n)
125             {
126                 writefln("%*s(null)", indent, "");
127                 return;
128             }
129             for (auto sis = n; sis; sis = sis.sibling)
130             {
131                 writef("%*s%x (%s bytes) ", indent, "",
132                     cast(void*) n, n.size);
133             }
134             writeln;
135             if (!n.left && !n.right) return;
136             recurse(n.left, indent + 4);
137             recurse(n.right, indent + 4);
138         }
139         recurse(root);
140     }
141 
142     private static void rotate(ref Node* parent, bool toRight)
143     {
144         assert(parent);
145         auto opposing = parent.kid[!toRight];
146         if (!opposing) return;
147         parent.kid[!toRight] = opposing.kid[toRight];
148         opposing.kid[toRight] = parent;
149         parent = opposing;
150     }
151 
152     // Inserts which into the tree, making it the new root
153     private void insertAsRoot(Node* which)
154     {
155         assert(which);
156         debug(std_experimental_allocator_free_tree)
157         {
158             assertValid;
159             scope(exit) assertValid;
160         }
161 
162         static void recurse(ref Node* where, Node* which)
163         {
164             if (!where)
165             {
166                 where = which;
167                 which.left = null;
168                 which.right = null;
169                 which.sibling = null;
170                 return;
171             }
172             if (which.size == where.size)
173             {
174                 // Special handling of duplicates
175                 which.sibling = where.sibling;
176                 where.sibling = which;
177                 which.left = null;
178                 which.right = null;
179                 return;
180             }
181             bool goRight = which.size > where.size;
182             recurse(where.kid[goRight], which);
183             rotate(where, !goRight);
184         }
185         recurse(root, which);
186     }
187 
188     private void assertValid()
189     {
190         debug(std_experimental_allocator_free_tree)
191         {
192             static bool isBST(Node* n, size_t lb = 0, size_t ub = size_t.max)
193             {
194                 if (!n) return true;
195                 for (auto sis = n.sibling; sis; sis = sis.sibling)
196                 {
197                     assert(n.size == sis.size);
198                     assert(sis.left is null);
199                     assert(sis.right is null);
200                 }
201                 return lb < n.size && n.size <= ub
202                     && isBST(n.left, lb, min(ub, n.size))
203                     && isBST(n.right, max(lb, n.size), ub);
204             }
205             if (isBST(root)) return;
206             dump;
207             assert(0);
208         }
209     }
210 
211     /**
212     The $(D FreeTree) is word aligned.
213     */
214     enum uint alignment = size_t.alignof;
215 
216     /**
217     The $(D FreeTree) allocator is noncopyable.
218     */
219     this(this) @disable;
220 
221     /**
222     The destructor of $(D FreeTree) releases all memory back to the parent
223     allocator.
224     */
225     static if (__traits(hasMember, ParentAllocator, "deallocate"))
226     ~this()
227     {
228         clear;
229     }
230 
231     /**
232     Returns $(D parent.goodAllocSize(max(Node.sizeof, s))).
233     */
234     static if (stateSize!ParentAllocator)
235         size_t goodAllocSize(size_t s)
236         {
237             return parent.goodAllocSize(max(Node.sizeof, s));
238         }
239     else
240         static size_t goodAllocSize(size_t s)
241         {
242             return parent.goodAllocSize(max(Node.sizeof, s));
243         }
244 
245     /**
246 
247     Allocates $(D n) bytes of memory. First consults the free tree, and returns
248     from it if a suitably sized block is found. Otherwise, the parent allocator
249     is tried. If allocation from the parent succeeds, the allocated block is
250     returned. Otherwise, the free tree tries an alternate strategy: If $(D
251     ParentAllocator) defines $(D deallocate), $(D FreeTree) releases all of its
252     contents and tries again.
253 
254     TODO: Splitting and coalescing should be implemented if $(D ParentAllocator) does not defined $(D deallocate).
255 
256     */
257     void[] allocate(size_t n)
258     {
259         assertValid;
260         if (n == 0) return null;
261 
262         immutable s = goodAllocSize(n);
263 
264         // Consult the free tree.
265         auto result = findAndRemove(root, s);
266         if (result.ptr) return result.ptr[0 .. n];
267 
268         // No block found, try the parent allocator.
269         result = parent.allocate(s);
270         if (result.ptr) return result.ptr[0 .. n];
271 
272         // Parent ran out of juice, desperation mode on
273         static if (__traits(hasMember, ParentAllocator, "deallocate"))
274         {
275             clear;
276             // Try parent allocator again.
277             result = parent.allocate(s);
278             if (result.ptr) return result.ptr[0 .. n];
279             return null;
280         }
281         else
282         {
283             // TODO: get smart here
284             return null;
285         }
286     }
287 
288     // Forwarding methods
289     mixin(forwardToMember("parent",
290         "allocateAll", "expand", "owns", "reallocate"));
291 
292     /** Places $(D b) into the free tree. */
293     bool deallocate(void[] b)
294     {
295         if (!b.ptr) return true;
296         auto which = cast(Node*) b.ptr;
297         which.size = goodAllocSize(b.length);
298         // deliberately don't initialize which.left and which.right
299         assert(which.size >= Node.sizeof);
300         insertAsRoot(which);
301         return true;
302     }
303 
304     @system unittest // build a complex free tree
305     {
306         import stdx.allocator.gc_allocator, std.range;
307         FreeTree!GCAllocator a;
308         uint[] sizes = [3008,704,1856,576,1632,672,832,1856,1120,2656,1216,672,
309             448,992,2400,1376,2688,2656,736,1440];
310         void[][] allocs;
311         foreach (s; sizes)
312             allocs ~= a.allocate(s);
313         foreach_reverse (b; allocs)
314         {
315             assert(b.ptr);
316             a.deallocate(b);
317         }
318         a.assertValid;
319         allocs = null;
320         foreach (s; sizes)
321             allocs ~= a.allocate(s);
322         assert(a.root is null);
323         a.assertValid;
324     }
325 
326     /** Defined if $(D ParentAllocator.deallocate) exists, and returns to it
327     all memory held in the free tree. */
328     static if (__traits(hasMember, ParentAllocator, "deallocate"))
329     void clear()
330     {
331         void recurse(Node* n)
332         {
333             if (!n) return;
334             recurse(n.left);
335             recurse(n.right);
336             parent.deallocate((cast(ubyte*) n)[0 .. n.size]);
337         }
338         recurse(root);
339         root = null;
340     }
341 
342     /**
343 
344     Defined if $(D ParentAllocator.deallocateAll) exists, and forwards to it.
345     Also nullifies the free tree (it's assumed the parent frees all memory
346     stil managed by the free tree).
347 
348     */
349     static if (__traits(hasMember, ParentAllocator, "deallocateAll"))
350     bool deallocateAll()
351     {
352         // This is easy, just nuke the root and deallocate all from the
353         // parent
354         root = null;
355         return parent.deallocateAll;
356     }
357 }
358 
359 @system unittest
360 {
361     import stdx.allocator.gc_allocator;
362     testAllocator!(() => FreeTree!GCAllocator());
363 }
364 
365 @system unittest // issue 16506
366 {
367     import stdx.allocator.gc_allocator : GCAllocator;
368     import stdx.allocator.mallocator : Mallocator;
369 
370     static void f(ParentAllocator)(size_t sz)
371     {
372         static FreeTree!ParentAllocator myAlloc;
373         byte[] _payload = cast(byte[]) myAlloc.allocate(sz);
374         assert(_payload, "_payload is null");
375         _payload[] = 0;
376         myAlloc.deallocate(_payload);
377     }
378 
379     f!Mallocator(33);
380     f!Mallocator(43);
381     f!GCAllocator(1);
382 }
383 
384 @system unittest // issue 16507
385 {
386     static struct MyAllocator
387     {
388         byte dummy;
389         static bool alive = true;
390         void[] allocate(size_t s) { return new byte[](s); }
391         bool deallocate(void[] ) { if (alive) assert(false); return true; }
392         enum alignment = size_t.sizeof;
393     }
394 
395     FreeTree!MyAllocator ft;
396     void[] x = ft.allocate(1);
397     ft.deallocate(x);
398     ft.allocate(1000);
399     MyAllocator.alive = false;
400 }
401 
402 @system unittest // "desperation mode"
403 {
404     uint myDeallocCounter = 0;
405 
406     struct MyAllocator
407     {
408         byte[] allocation;
409         void[] allocate(size_t s)
410         {
411             if (allocation.ptr) return null;
412             allocation = new byte[](s);
413             return allocation;
414         }
415         bool deallocate(void[] )
416         {
417             ++myDeallocCounter;
418             allocation = null;
419             return true;
420         }
421         enum alignment = size_t.sizeof;
422     }
423 
424     FreeTree!MyAllocator ft;
425     void[] x = ft.allocate(1);
426     ft.deallocate(x);
427     assert(myDeallocCounter == 0);
428     x = ft.allocate(1000); // Triggers "desperation mode".
429     assert(myDeallocCounter == 1);
430     assert(x.ptr);
431     void[] y = ft.allocate(1000); /* Triggers "desperation mode" but there's
432         nothing to deallocate so MyAllocator can't deliver. */
433     assert(myDeallocCounter == 1);
434     assert(y.ptr is null);
435 }