The OpenD Programming Language

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