1 // Written in the D programming language.
3 Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/_free_tree.d)
5 module std.experimental.allocator.building_blocks.free_tree;
7 import std.experimental.allocator.common;
9 //debug = std_experimental_allocator_free_tree;
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
19 Common uses of `FreeTree` include:
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.)
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.
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.
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.
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.
51 The following methods are defined if `ParentAllocator` defines them, and forward to it: `allocateAll`, `expand`, `owns`, `reallocate`.
53 struct FreeTree(ParentAllocator)
55 static assert(ParentAllocator.alignment % size_t.alignof == 0,
56 "FreeTree must be on top of a word-aligned allocator");
58 import std.algorithm.comparison : min, max;
59 import std.algorithm.mutation : swap;
60 import std.traits : hasMember;
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
72 ref Node* left() { return kid[0]; }
73 ref Node* right() { return kid[1]; }
76 // Removes "which" from the tree, returns the memory it occupied
77 private void[] remove(ref Node* 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;
86 // result has two kids
88 // Crude randomization: alternate left/right choices
90 auto newRoot = which.kid[toggler], orphan = which.kid[!toggler];
92 for (Node* n = void; (n = newRoot.kid[!toggler]) !is null; )
96 newRoot.kid[!toggler] = orphan;
101 private void[] findAndRemove(ref Node* n, size_t s)
106 if (auto sis = n.sibling)
108 // Nice, give away one from the freelist
109 auto result = (cast(ubyte*) sis)[0 .. sis.size];
110 n.sibling = sis.sibling;
115 return findAndRemove(n.kid[s > n.size], s);
118 debug(std_experimental_allocator_free_tree)
121 import std.stdio : writef, writefln, writeln;
122 writeln(typeof(this).stringof, "@", &this, " {");
123 scope(exit) writeln("}");
127 static void recurse(Node* n, uint indent = 4)
131 writefln("%*s(null)", indent, "");
134 for (auto sis = n; sis; sis = sis.sibling)
136 writef("%*s%x (%s bytes) ", indent, "",
137 cast(void*) n, n.size);
140 if (!n.left && !n.right) return;
141 recurse(n.left, indent + 4);
142 recurse(n.right, indent + 4);
147 private string formatSizes()
150 void recurse(Node* n)
157 import std.conv : to;
158 result ~= to!string(n.size);
159 for (auto sis = n.sibling; sis; sis = sis.sibling)
163 if (n.left || n.right)
173 return result ~= ")";
176 private static void rotate(ref Node* parent, bool toRight)
179 auto opposing = parent.kid[!toRight];
180 if (!opposing) return;
181 parent.kid[!toRight] = opposing.kid[toRight];
182 opposing.kid[toRight] = parent;
186 // Inserts which into the tree, making it the new root
187 private void insertAsRoot(Node* which)
190 debug(std_experimental_allocator_free_tree)
193 scope(exit) assertValid;
196 static void recurse(ref Node* where, Node* which)
203 which.sibling = null;
206 if (which.size == where.size)
208 // Special handling of duplicates
209 which.sibling = where.sibling;
210 where.sibling = which;
215 bool goRight = which.size > where.size;
216 recurse(where.kid[goRight], which);
217 rotate(where, !goRight);
219 recurse(root, which);
222 private void assertValid()
224 debug(std_experimental_allocator_free_tree)
226 static bool isBST(Node* n, size_t lb = 0, size_t ub = size_t.max)
229 for (auto sis = n.sibling; sis; sis = sis.sibling)
231 assert(n.size == sis.size);
232 assert(sis.left is null);
233 assert(sis.right is null);
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);
239 if (isBST(root)) return;
246 The `FreeTree` is word aligned.
248 enum uint alignment = size_t.alignof;
251 The `FreeTree` allocator is noncopyable.
256 The destructor of `FreeTree` releases all memory back to the parent
259 static if (hasMember!(ParentAllocator, "deallocate"))
266 Returns $(D parent.goodAllocSize(max(Node.sizeof, s))).
268 static if (stateSize!ParentAllocator)
269 size_t goodAllocSize(size_t s)
271 return parent.goodAllocSize(max(Node.sizeof, s));
274 static size_t goodAllocSize(size_t s)
276 return parent.goodAllocSize(max(Node.sizeof, s));
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.
288 TODO: Splitting and coalescing should be implemented if `ParentAllocator` does not defined `deallocate`.
291 void[] allocate(size_t n)
294 if (n == 0) return null;
296 immutable s = goodAllocSize(n);
298 // Consult the free tree.
299 auto result = findAndRemove(root, s);
300 if (result.ptr) return result.ptr[0 .. n];
302 // No block found, try the parent allocator.
303 result = parent.allocate(s);
304 if (result.ptr) return result.ptr[0 .. n];
306 // Parent ran out of juice, desperation mode on
307 static if (hasMember!(ParentAllocator, "deallocate"))
310 // Try parent allocator again.
311 result = parent.allocate(s);
312 if (result.ptr) return result.ptr[0 .. n];
317 // TODO: get smart here
322 // Forwarding methods
323 mixin(forwardToMember("parent",
324 "allocateAll", "expand", "owns", "reallocate"));
326 /** Places `b` into the free tree. */
327 bool deallocate(void[] b)
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);
338 @system unittest // test a few simple configurations
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);
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);
359 @system unittest // build a complex free tree
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];
367 allocs ~= a.allocate(s);
368 foreach_reverse (b; allocs)
371 () nothrow @nogc { a.deallocate(b); }();
376 allocs ~= a.allocate(s);
377 assert(a.root is null);
381 /** Defined if `ParentAllocator.deallocate` exists, and returns to it
382 all memory held in the free tree. */
383 static if (hasMember!(ParentAllocator, "deallocate"))
386 void recurse(Node* n)
391 parent.deallocate((cast(ubyte*) n)[0 .. n.size]);
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).
404 static if (hasMember!(ParentAllocator, "deallocateAll"))
407 // This is easy, just nuke the root and deallocate all from the
410 return parent.deallocateAll;
414 version (StdUnittest)
417 import std.experimental.allocator.gc_allocator;
418 testAllocator!(() => FreeTree!GCAllocator());
421 // https://issues.dlang.org/show_bug.cgi?id=16506
424 import std.experimental.allocator.gc_allocator : GCAllocator;
425 import std.experimental.allocator.mallocator : Mallocator;
427 static void f(ParentAllocator)(size_t sz)
429 static FreeTree!ParentAllocator myAlloc;
430 byte[] _payload = cast(byte[]) myAlloc.allocate(sz);
431 assert(_payload, "_payload is null");
433 () nothrow @nogc { myAlloc.deallocate(_payload); }();
441 // https://issues.dlang.org/show_bug.cgi?id=16507
444 static struct MyAllocator
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;
453 FreeTree!MyAllocator ft;
454 void[] x = ft.allocate(1);
455 () nothrow @nogc { ft.deallocate(x); }();
457 MyAllocator.alive = false;
460 @system unittest // "desperation mode"
462 uint myDeallocCounter = 0;
467 void[] allocate(size_t s)
469 if (allocation.ptr) return null;
470 allocation = new byte[](s);
473 bool deallocate(void[] )
479 enum alignment = size_t.sizeof;
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);
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);
497 import std.experimental.allocator.gc_allocator;
498 FreeTree!GCAllocator a;
500 assert((() nothrow @safe @nogc => a.goodAllocSize(1))() == typeof(*a.root).sizeof);
505 import std.experimental.allocator.building_blocks.region : Region;
507 auto a = FreeTree!(Region!())(Region!()(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())());