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 }