| // Written in the D programming language. | |
| /** | |
| Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/_free_tree.d) | |
| */ | |
| module std.experimental.allocator.building_blocks.free_tree; | |
| import std.experimental.allocator.common; | |
| //debug = std_experimental_allocator_free_tree; | |
| /** | |
| The Free Tree allocator, stackable on top of any other allocator, bears | |
| similarity with the free list allocator. Instead of a singly-linked list of | |
| previously freed blocks, it maintains a binary search tree. This allows the | |
| Free Tree allocator to manage blocks of arbitrary lengths and search them | |
| efficiently. | |
| Common uses of `FreeTree` include: | |
| $(UL | |
| $(LI Adding `deallocate` capability to an allocator that lacks it (such as simple regions).) | |
| $(LI Getting the benefits of multiple adaptable freelists that do not need to | |
| be tuned for one specific size but insted automatically adapts itself to | |
| frequently used sizes.) | |
| ) | |
| The free tree has special handling of duplicates (a singly-linked list per | |
| node) in anticipation of large number of duplicates. Allocation time from the | |
| free tree is expected to be $(BIGOH log n) where `n` is the number of | |
| distinct sizes (not total nodes) kept in the free tree. | |
| Allocation requests first search the tree for a buffer of suitable size | |
| deallocated in the past. If a match is found, the node is removed from the tree | |
| and the memory is returned. Otherwise, the allocation is directed to $(D | |
| ParentAllocator). If at this point `ParentAllocator` also fails to allocate, | |
| `FreeTree` frees everything and then tries the parent allocator again. | |
| Upon deallocation, the deallocated block is inserted in the internally | |
| maintained free tree (not returned to the parent). The free tree is not kept | |
| balanced. Instead, it has a last-in-first-out flavor because newly inserted | |
| blocks are rotated to the root of the tree. That way allocations are cache | |
| friendly and also frequently used sizes are more likely to be found quickly, | |
| whereas seldom used sizes migrate to the leaves of the tree. | |
| `FreeTree` rounds up small allocations to at least $(D 4 * size_t.sizeof), | |
| which on 64-bit system is one cache line size. If very small objects need to | |
| be efficiently allocated, the `FreeTree` should be fronted with an | |
| appropriate small object allocator. | |
| The following methods are defined if `ParentAllocator` defines them, and forward to it: `allocateAll`, `expand`, `owns`, `reallocate`. | |
| */ | |
| struct FreeTree(ParentAllocator) | |
| { | |
| static assert(ParentAllocator.alignment % size_t.alignof == 0, | |
| "FreeTree must be on top of a word-aligned allocator"); | |
| import std.algorithm.comparison : min, max; | |
| import std.algorithm.mutation : swap; | |
| import std.traits : hasMember; | |
| // State | |
| static if (stateSize!ParentAllocator) private ParentAllocator parent; | |
| else private alias parent = ParentAllocator.instance; | |
| private Node* root; // that's the entire added state | |
| private struct Node | |
| { | |
| Node*[2] kid; | |
| Node* sibling; | |
| size_t size; | |
| ref Node* left() { return kid[0]; } | |
| ref Node* right() { return kid[1]; } | |
| } | |
| // Removes "which" from the tree, returns the memory it occupied | |
| private void[] remove(ref Node* which) | |
| { | |
| assert(which); | |
| assert(!which.sibling); | |
| auto result = (cast(ubyte*) which)[0 .. which.size]; | |
| if (!which.right) which = which.left; | |
| else if (!which.left) which = which.right; | |
| else | |
| { | |
| // result has two kids | |
| static bool toggler; | |
| // Crude randomization: alternate left/right choices | |
| toggler = !toggler; | |
| auto newRoot = which.kid[toggler], orphan = which.kid[!toggler]; | |
| which = newRoot; | |
| for (Node* n = void; (n = newRoot.kid[!toggler]) !is null; ) | |
| { | |
| newRoot = n; | |
| } | |
| newRoot.kid[!toggler] = orphan; | |
| } | |
| return result; | |
| } | |
| private void[] findAndRemove(ref Node* n, size_t s) | |
| { | |
| if (!n) return null; | |
| if (s == n.size) | |
| { | |
| if (auto sis = n.sibling) | |
| { | |
| // Nice, give away one from the freelist | |
| auto result = (cast(ubyte*) sis)[0 .. sis.size]; | |
| n.sibling = sis.sibling; | |
| return result; | |
| } | |
| return remove(n); | |
| } | |
| return findAndRemove(n.kid[s > n.size], s); | |
| } | |
| debug(std_experimental_allocator_free_tree) | |
| private void dump() | |
| { | |
| import std.stdio : writef, writefln, writeln; | |
| writeln(typeof(this).stringof, "@", &this, " {"); | |
| scope(exit) writeln("}"); | |
| if (!root) return; | |
| static void recurse(Node* n, uint indent = 4) | |
| { | |
| if (!n) | |
| { | |
| writefln("%*s(null)", indent, ""); | |
| return; | |
| } | |
| for (auto sis = n; sis; sis = sis.sibling) | |
| { | |
| writef("%*s%x (%s bytes) ", indent, "", | |
| cast(void*) n, n.size); | |
| } | |
| writeln; | |
| if (!n.left && !n.right) return; | |
| recurse(n.left, indent + 4); | |
| recurse(n.right, indent + 4); | |
| } | |
| recurse(root); | |
| } | |
| private string formatSizes() | |
| { | |
| string result = "("; | |
| void recurse(Node* n) | |
| { | |
| if (!n) | |
| { | |
| result ~= "_"; | |
| return; | |
| } | |
| import std.conv : to; | |
| result ~= to!string(n.size); | |
| for (auto sis = n.sibling; sis; sis = sis.sibling) | |
| { | |
| result ~= "+moar"; | |
| } | |
| if (n.left || n.right) | |
| { | |
| result ~= " ("; | |
| recurse(n.left); | |
| result ~= ' '; | |
| recurse(n.right); | |
| result ~= ")"; | |
| } | |
| } | |
| recurse(root); | |
| return result ~= ")"; | |
| } | |
| private static void rotate(ref Node* parent, bool toRight) | |
| { | |
| assert(parent); | |
| auto opposing = parent.kid[!toRight]; | |
| if (!opposing) return; | |
| parent.kid[!toRight] = opposing.kid[toRight]; | |
| opposing.kid[toRight] = parent; | |
| parent = opposing; | |
| } | |
| // Inserts which into the tree, making it the new root | |
| private void insertAsRoot(Node* which) | |
| { | |
| assert(which); | |
| debug(std_experimental_allocator_free_tree) | |
| { | |
| assertValid; | |
| scope(exit) assertValid; | |
| } | |
| static void recurse(ref Node* where, Node* which) | |
| { | |
| if (!where) | |
| { | |
| where = which; | |
| which.left = null; | |
| which.right = null; | |
| which.sibling = null; | |
| return; | |
| } | |
| if (which.size == where.size) | |
| { | |
| // Special handling of duplicates | |
| which.sibling = where.sibling; | |
| where.sibling = which; | |
| which.left = null; | |
| which.right = null; | |
| return; | |
| } | |
| bool goRight = which.size > where.size; | |
| recurse(where.kid[goRight], which); | |
| rotate(where, !goRight); | |
| } | |
| recurse(root, which); | |
| } | |
| private void assertValid() | |
| { | |
| debug(std_experimental_allocator_free_tree) | |
| { | |
| static bool isBST(Node* n, size_t lb = 0, size_t ub = size_t.max) | |
| { | |
| if (!n) return true; | |
| for (auto sis = n.sibling; sis; sis = sis.sibling) | |
| { | |
| assert(n.size == sis.size); | |
| assert(sis.left is null); | |
| assert(sis.right is null); | |
| } | |
| return lb < n.size && n.size <= ub | |
| && isBST(n.left, lb, min(ub, n.size)) | |
| && isBST(n.right, max(lb, n.size), ub); | |
| } | |
| if (isBST(root)) return; | |
| dump; | |
| assert(0); | |
| } | |
| } | |
| /** | |
| The `FreeTree` is word aligned. | |
| */ | |
| enum uint alignment = size_t.alignof; | |
| /** | |
| The `FreeTree` allocator is noncopyable. | |
| */ | |
| this(this) @disable; | |
| /** | |
| The destructor of `FreeTree` releases all memory back to the parent | |
| allocator. | |
| */ | |
| static if (hasMember!(ParentAllocator, "deallocate")) | |
| ~this() | |
| { | |
| clear; | |
| } | |
| /** | |
| Returns $(D parent.goodAllocSize(max(Node.sizeof, s))). | |
| */ | |
| static if (stateSize!ParentAllocator) | |
| size_t goodAllocSize(size_t s) | |
| { | |
| return parent.goodAllocSize(max(Node.sizeof, s)); | |
| } | |
| else | |
| static size_t goodAllocSize(size_t s) | |
| { | |
| return parent.goodAllocSize(max(Node.sizeof, s)); | |
| } | |
| /** | |
| Allocates `n` bytes of memory. First consults the free tree, and returns | |
| from it if a suitably sized block is found. Otherwise, the parent allocator | |
| is tried. If allocation from the parent succeeds, the allocated block is | |
| returned. Otherwise, the free tree tries an alternate strategy: If $(D | |
| ParentAllocator) defines `deallocate`, `FreeTree` releases all of its | |
| contents and tries again. | |
| TODO: Splitting and coalescing should be implemented if `ParentAllocator` does not defined `deallocate`. | |
| */ | |
| void[] allocate(size_t n) | |
| { | |
| assertValid; | |
| if (n == 0) return null; | |
| immutable s = goodAllocSize(n); | |
| // Consult the free tree. | |
| auto result = findAndRemove(root, s); | |
| if (result.ptr) return result.ptr[0 .. n]; | |
| // No block found, try the parent allocator. | |
| result = parent.allocate(s); | |
| if (result.ptr) return result.ptr[0 .. n]; | |
| // Parent ran out of juice, desperation mode on | |
| static if (hasMember!(ParentAllocator, "deallocate")) | |
| { | |
| clear; | |
| // Try parent allocator again. | |
| result = parent.allocate(s); | |
| if (result.ptr) return result.ptr[0 .. n]; | |
| return null; | |
| } | |
| else | |
| { | |
| // TODO: get smart here | |
| return null; | |
| } | |
| } | |
| // Forwarding methods | |
| mixin(forwardToMember("parent", | |
| "allocateAll", "expand", "owns", "reallocate")); | |
| /** Places `b` into the free tree. */ | |
| bool deallocate(void[] b) | |
| { | |
| if (!b.ptr) return true; | |
| auto which = cast(Node*) b.ptr; | |
| which.size = goodAllocSize(b.length); | |
| // deliberately don't initialize which.left and which.right | |
| assert(which.size >= Node.sizeof); | |
| insertAsRoot(which); | |
| return true; | |
| } | |
| @system unittest // test a few simple configurations | |
| { | |
| import std.experimental.allocator.gc_allocator; | |
| FreeTree!GCAllocator a; | |
| auto b1 = a.allocate(10000); | |
| auto b2 = a.allocate(20000); | |
| auto b3 = a.allocate(30000); | |
| assert(b1.ptr && b2.ptr && b3.ptr); | |
| () nothrow @nogc { a.deallocate(b1); }(); | |
| () nothrow @nogc { a.deallocate(b3); }(); | |
| () nothrow @nogc { a.deallocate(b2); }(); | |
| assert(a.formatSizes == "(20480 (12288 32768))", a.formatSizes); | |
| b1 = a.allocate(10000); | |
| assert(a.formatSizes == "(20480 (_ 32768))", a.formatSizes); | |
| b1 = a.allocate(30000); | |
| assert(a.formatSizes == "(20480)", a.formatSizes); | |
| b1 = a.allocate(20000); | |
| assert(a.formatSizes == "(_)", a.formatSizes); | |
| } | |
| @system unittest // build a complex free tree | |
| { | |
| import std.experimental.allocator.gc_allocator, std.range; | |
| FreeTree!GCAllocator a; | |
| uint[] sizes = [3008,704,1856,576,1632,672,832,1856,1120,2656,1216,672, | |
| 448,992,2400,1376,2688,2656,736,1440]; | |
| void[][] allocs; | |
| foreach (s; sizes) | |
| allocs ~= a.allocate(s); | |
| foreach_reverse (b; allocs) | |
| { | |
| assert(b.ptr); | |
| () nothrow @nogc { a.deallocate(b); }(); | |
| } | |
| a.assertValid; | |
| allocs = null; | |
| foreach (s; sizes) | |
| allocs ~= a.allocate(s); | |
| assert(a.root is null); | |
| a.assertValid; | |
| } | |
| /** Defined if `ParentAllocator.deallocate` exists, and returns to it | |
| all memory held in the free tree. */ | |
| static if (hasMember!(ParentAllocator, "deallocate")) | |
| void clear() | |
| { | |
| void recurse(Node* n) | |
| { | |
| if (!n) return; | |
| recurse(n.left); | |
| recurse(n.right); | |
| parent.deallocate((cast(ubyte*) n)[0 .. n.size]); | |
| } | |
| recurse(root); | |
| root = null; | |
| } | |
| /** | |
| Defined if `ParentAllocator.deallocateAll` exists, and forwards to it. | |
| Also nullifies the free tree (it's assumed the parent frees all memory | |
| stil managed by the free tree). | |
| */ | |
| static if (hasMember!(ParentAllocator, "deallocateAll")) | |
| bool deallocateAll() | |
| { | |
| // This is easy, just nuke the root and deallocate all from the | |
| // parent | |
| root = null; | |
| return parent.deallocateAll; | |
| } | |
| } | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| import std.experimental.allocator.gc_allocator; | |
| testAllocator!(() => FreeTree!GCAllocator()); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=16506 | |
| @system unittest | |
| { | |
| import std.experimental.allocator.gc_allocator : GCAllocator; | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| static void f(ParentAllocator)(size_t sz) | |
| { | |
| static FreeTree!ParentAllocator myAlloc; | |
| byte[] _payload = cast(byte[]) myAlloc.allocate(sz); | |
| assert(_payload, "_payload is null"); | |
| _payload[] = 0; | |
| () nothrow @nogc { myAlloc.deallocate(_payload); }(); | |
| } | |
| f!Mallocator(33); | |
| f!Mallocator(43); | |
| f!GCAllocator(1); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=16507 | |
| @system unittest | |
| { | |
| static struct MyAllocator | |
| { | |
| byte dummy; | |
| static bool alive = true; | |
| void[] allocate(size_t s) { return new byte[](s); } | |
| bool deallocate(void[] ) { if (alive) assert(false); return true; } | |
| enum alignment = size_t.sizeof; | |
| } | |
| FreeTree!MyAllocator ft; | |
| void[] x = ft.allocate(1); | |
| () nothrow @nogc { ft.deallocate(x); }(); | |
| ft.allocate(1000); | |
| MyAllocator.alive = false; | |
| } | |
| @system unittest // "desperation mode" | |
| { | |
| uint myDeallocCounter = 0; | |
| struct MyAllocator | |
| { | |
| byte[] allocation; | |
| void[] allocate(size_t s) | |
| { | |
| if (allocation.ptr) return null; | |
| allocation = new byte[](s); | |
| return allocation; | |
| } | |
| bool deallocate(void[] ) | |
| { | |
| ++myDeallocCounter; | |
| allocation = null; | |
| return true; | |
| } | |
| enum alignment = size_t.sizeof; | |
| } | |
| FreeTree!MyAllocator ft; | |
| void[] x = ft.allocate(1); | |
| () nothrow @nogc { ft.deallocate(x); }(); | |
| assert(myDeallocCounter == 0); | |
| x = ft.allocate(1000); // Triggers "desperation mode". | |
| assert(myDeallocCounter == 1); | |
| assert(x.ptr); | |
| void[] y = ft.allocate(1000); /* Triggers "desperation mode" but there's | |
| nothing to deallocate so MyAllocator can't deliver. */ | |
| assert(myDeallocCounter == 1); | |
| assert(y.ptr is null); | |
| } | |
| @system unittest | |
| { | |
| import std.experimental.allocator.gc_allocator; | |
| FreeTree!GCAllocator a; | |
| assert((() nothrow @safe @nogc => a.goodAllocSize(1))() == typeof(*a.root).sizeof); | |
| } | |
| @system unittest | |
| { | |
| import std.experimental.allocator.building_blocks.region : BorrowedRegion; | |
| auto a = FreeTree!(BorrowedRegion!())(BorrowedRegion!()(new ubyte[1024 * 64])); | |
| auto b = a.allocate(42); | |
| assert(b.length == 42); | |
| assert((() pure nothrow @safe @nogc => a.expand(b, 22))()); | |
| assert(b.length == 64); | |
| assert((() nothrow @nogc => a.reallocate(b, 100))()); | |
| assert(b.length == 100); | |
| assert((() nothrow @nogc => a.deallocateAll())()); | |
| } |