1 /// 2 module stdx.allocator.building_blocks.allocator_list; 3 4 import stdx.allocator.building_blocks.null_allocator; 5 import stdx.allocator.common; 6 import stdx.allocator.gc_allocator; 7 version(unittest) import std.stdio; 8 9 // Turn this on for debugging 10 // debug = allocator_list; 11 12 /** 13 14 Given an $(LINK2 https://en.wikipedia.org/wiki/Factory_(object-oriented_programming), 15 object factory) of type `Factory` or a factory function 16 `factoryFunction`, and optionally also `BookkeepingAllocator` as a supplemental 17 allocator for bookkeeping, `AllocatorList` creates an allocator that lazily 18 creates as many allocators are needed for satisfying client allocation requests. 19 20 An embedded list builds a most-recently-used strategy: the most recent 21 allocators used in calls to either `allocate`, `owns` (successful calls 22 only), or `deallocate` are tried for new allocations in order of their most 23 recent use. Thus, although core operations take in theory $(BIGOH k) time for 24 $(D k) allocators in current use, in many workloads the factor is sublinear. 25 Details of the actual strategy may change in future releases. 26 27 `AllocatorList` is primarily intended for coarse-grained handling of 28 allocators, i.e. the number of allocators in the list is expected to be 29 relatively small compared to the number of allocations handled by each 30 allocator. However, the per-allocator overhead is small so using 31 `AllocatorList` with a large number of allocators should be satisfactory as long 32 as the most-recently-used strategy is fast enough for the application. 33 34 `AllocatorList` makes an effort to return allocated memory back when no 35 longer used. It does so by destroying empty allocators. However, in order to 36 avoid thrashing (excessive creation/destruction of allocators under certain use 37 patterns), it keeps unused allocators for a while. 38 39 Params: 40 factoryFunction = A function or template function (including function literals). 41 New allocators are created by calling `factoryFunction(n)` with strictly 42 positive numbers `n`. Delegates that capture their enviroment are not created 43 amid concerns regarding garbage creation for the environment. When the factory 44 needs state, a `Factory` object should be used. 45 46 BookkeepingAllocator = Allocator used for storing bookkeeping data. The size of 47 bookkeeping data is proportional to the number of allocators. If $(D 48 BookkeepingAllocator) is $(D NullAllocator), then $(D AllocatorList) is 49 "ouroboros-style", i.e. it keeps the bookkeeping data in memory obtained from 50 the allocators themselves. Note that for ouroboros-style management, the size 51 $(D n) passed to $(D make) will be occasionally different from the size 52 requested by client code. 53 54 Factory = Type of a factory object that returns new allocators on a need 55 basis. For an object $(D sweatshop) of type $(D Factory), `sweatshop(n)` should 56 return an allocator able to allocate at least `n` bytes (i.e. `Factory` must 57 define `opCall(size_t)` to return an allocator object). Usually the capacity of 58 allocators created should be much larger than $(D n) such that an allocator can 59 be used for many subsequent allocations. $(D n) is passed only to ensure the 60 minimum necessary for the next allocation. The factory object is allowed to hold 61 state, which will be stored inside `AllocatorList` as a direct `public` member 62 called `factory`. 63 64 */ 65 struct AllocatorList(Factory, BookkeepingAllocator = GCAllocator) 66 { 67 import mir.conv : emplace; 68 import stdx.allocator.building_blocks.stats_collector 69 : StatsCollector, Options; 70 import stdx.allocator.internal : Ternary; 71 72 private enum ouroboros = is(BookkeepingAllocator == NullAllocator); 73 74 /** 75 Alias for `typeof(Factory()(1))`, i.e. the type of the individual 76 allocators. 77 */ 78 alias Allocator = typeof(Factory.init(size_t(1))); 79 // Allocator used internally 80 private alias SAllocator = StatsCollector!(Allocator, Options.bytesUsed); 81 82 private static struct Node 83 { 84 // Allocator in this node 85 SAllocator a; 86 Node* next; 87 88 @disable this(this); 89 90 // Is this node unused? 91 void setUnused() { next = &this; } 92 bool unused() const { return next is &this; } 93 94 // Just forward everything to the allocator 95 alias a this; 96 } 97 98 /** 99 If $(D BookkeepingAllocator) is not $(D NullAllocator), $(D bkalloc) is 100 defined and accessible. 101 */ 102 103 // State is stored in an array, but it has a list threaded through it by 104 // means of "nextIdx". 105 106 // state 107 static if (!ouroboros) 108 { 109 static if (stateSize!BookkeepingAllocator) BookkeepingAllocator bkalloc; 110 else alias bkalloc = BookkeepingAllocator.instance; 111 } 112 static if (stateSize!Factory) 113 { 114 Factory factory; 115 } 116 private Node[] allocators; 117 private Node* root; 118 119 static if (stateSize!Factory) 120 { 121 private auto make(size_t n) { return factory(n); } 122 } 123 else 124 { 125 private auto make(size_t n) { Factory f; return f(n); } 126 } 127 128 /** 129 Constructs an `AllocatorList` given a factory object. This constructor is 130 defined only if `Factory` has state. 131 */ 132 static if (stateSize!Factory) 133 this(ref Factory plant) 134 { 135 factory = plant; 136 } 137 /// Ditto 138 static if (stateSize!Factory) 139 this(Factory plant) 140 { 141 factory = plant; 142 } 143 144 static if (__traits(hasMember, Allocator, "deallocateAll") 145 && __traits(hasMember, Allocator, "owns")) 146 ~this() 147 { 148 deallocateAll; 149 } 150 151 /** 152 The alignment offered. 153 */ 154 enum uint alignment = Allocator.alignment; 155 156 /** 157 Allocate a block of size $(D s). First tries to allocate from the existing 158 list of already-created allocators. If neither can satisfy the request, 159 creates a new allocator by calling $(D make(s)) and delegates the request 160 to it. However, if the allocation fresh off a newly created allocator 161 fails, subsequent calls to $(D allocate) will not cause more calls to $(D 162 make). 163 */ 164 void[] allocate(size_t s) 165 { 166 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 167 { 168 auto result = n.allocate(s); 169 if (result.length != s) continue; 170 // Bring to front if not already 171 if (root != n) 172 { 173 *p = n.next; 174 n.next = root; 175 root = n; 176 } 177 return result; 178 } 179 // Can't allocate from the current pool. Check if we just added a new 180 // allocator, in that case it won't do any good to add yet another. 181 if (root && root.empty == Ternary.yes) 182 { 183 // no can do 184 return null; 185 } 186 // Add a new allocator 187 if (auto a = addAllocator(s)) 188 { 189 auto result = a.allocate(s); 190 assert(owns(result) == Ternary.yes || !result.ptr); 191 return result; 192 } 193 return null; 194 } 195 196 private void moveAllocators(void[] newPlace) 197 { 198 assert(newPlace.ptr.alignedAt(Node.alignof)); 199 assert(newPlace.length % Node.sizeof == 0); 200 auto newAllocators = cast(Node[]) newPlace; 201 assert(allocators.length <= newAllocators.length); 202 203 // Move allocators 204 foreach (i, ref e; allocators) 205 { 206 if (e.unused) 207 { 208 newAllocators[i].setUnused; 209 continue; 210 } 211 import core.stdc..string : memcpy; 212 memcpy(&newAllocators[i].a, &e.a, e.a.sizeof); 213 if (e.next) 214 { 215 newAllocators[i].next = newAllocators.ptr 216 + (e.next - allocators.ptr); 217 } 218 else 219 { 220 newAllocators[i].next = null; 221 } 222 } 223 224 // Mark the unused portion as unused 225 foreach (i; allocators.length .. newAllocators.length) 226 { 227 newAllocators[i].setUnused; 228 } 229 auto toFree = allocators; 230 231 // Change state 232 root = newAllocators.ptr + (root - allocators.ptr); 233 allocators = newAllocators; 234 235 // Free the olden buffer 236 static if (ouroboros) 237 { 238 static if (__traits(hasMember, Allocator, "deallocate") 239 && __traits(hasMember, Allocator, "owns")) 240 deallocate(toFree); 241 } 242 else 243 { 244 bkalloc.deallocate(toFree); 245 } 246 } 247 248 static if (ouroboros) 249 private Node* addAllocator(size_t atLeastBytes) 250 { 251 void[] t = allocators; 252 static if (__traits(hasMember, Allocator, "expand") 253 && __traits(hasMember, Allocator, "owns")) 254 { 255 immutable bool expanded = t && this.expand(t, Node.sizeof); 256 } 257 else 258 { 259 enum expanded = false; 260 } 261 if (expanded) 262 { 263 import core.stdc..string : memcpy; 264 assert(t.length % Node.sizeof == 0); 265 assert(t.ptr.alignedAt(Node.alignof)); 266 allocators = cast(Node[]) t; 267 allocators[$ - 1].setUnused; 268 auto newAlloc = SAllocator(make(atLeastBytes)); 269 memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof); 270 emplace(&newAlloc); 271 } 272 else 273 { 274 immutable toAlloc = (allocators.length + 1) * Node.sizeof 275 + atLeastBytes + 128; 276 auto newAlloc = SAllocator(make(toAlloc)); 277 auto newPlace = newAlloc.allocate( 278 (allocators.length + 1) * Node.sizeof); 279 if (!newPlace) return null; 280 moveAllocators(newPlace); 281 import core.stdc..string : memcpy; 282 memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof); 283 emplace(&newAlloc); 284 assert(allocators[$ - 1].owns(allocators) == Ternary.yes); 285 } 286 // Insert as new root 287 if (root != &allocators[$ - 1]) 288 { 289 allocators[$ - 1].next = root; 290 root = &allocators[$ - 1]; 291 } 292 else 293 { 294 // This is the first one 295 root.next = null; 296 } 297 assert(!root.unused); 298 return root; 299 } 300 301 static if (!ouroboros) 302 private Node* addAllocator(size_t atLeastBytes) 303 { 304 void[] t = allocators; 305 static if (__traits(hasMember, BookkeepingAllocator, "expand")) 306 immutable bool expanded = bkalloc.expand(t, Node.sizeof); 307 else 308 immutable bool expanded = false; 309 if (expanded) 310 { 311 assert(t.length % Node.sizeof == 0); 312 assert(t.ptr.alignedAt(Node.alignof)); 313 allocators = cast(Node[]) t; 314 allocators[$ - 1].setUnused; 315 } 316 else 317 { 318 // Could not expand, create a new block 319 t = bkalloc.allocate((allocators.length + 1) * Node.sizeof); 320 assert(t.length % Node.sizeof == 0); 321 if (!t.ptr) return null; 322 moveAllocators(t); 323 } 324 assert(allocators[$ - 1].unused); 325 auto newAlloc = SAllocator(make(atLeastBytes)); 326 import core.stdc..string : memcpy; 327 memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof); 328 emplace(&newAlloc); 329 // Creation succeeded, insert as root 330 if (allocators.length == 1) 331 allocators[$ - 1].next = null; 332 else 333 allocators[$ - 1].next = root; 334 assert(allocators[$ - 1].a.bytesUsed == 0); 335 root = &allocators[$ - 1]; 336 return root; 337 } 338 339 /** 340 Defined only if `Allocator` defines `owns`. Tries each allocator in 341 turn, in most-recently-used order. If the owner is found, it is moved to 342 the front of the list as a side effect under the assumption it will be used 343 soon. 344 345 Returns: `Ternary.yes` if one allocator was found to return `Ternary.yes`, 346 `Ternary.no` if all component allocators returned `Ternary.no`, and 347 `Ternary.unknown` if no allocator returned `Ternary.yes` and at least one 348 returned `Ternary.unknown`. 349 */ 350 static if (__traits(hasMember, Allocator, "owns")) 351 Ternary owns(void[] b) 352 { 353 auto result = Ternary.no; 354 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 355 { 356 immutable t = n.owns(b); 357 if (t != Ternary.yes) 358 { 359 if (t == Ternary.unknown) result = t; 360 continue; 361 } 362 // Move the owner to front, speculating it'll be used 363 if (n != root) 364 { 365 *p = n.next; 366 n.next = root; 367 root = n; 368 } 369 return Ternary.yes; 370 } 371 return result; 372 } 373 374 /** 375 Defined only if $(D Allocator.expand) is defined. Finds the owner of $(D b) 376 and calls $(D expand) for it. The owner is not brought to the head of the 377 list. 378 */ 379 static if (__traits(hasMember, Allocator, "expand") 380 && __traits(hasMember, Allocator, "owns")) 381 bool expand(ref void[] b, size_t delta) 382 { 383 if (!b.ptr) return delta == 0; 384 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 385 { 386 if (n.owns(b) == Ternary.yes) return n.expand(b, delta); 387 } 388 return false; 389 } 390 391 /** 392 Defined only if $(D Allocator.reallocate) is defined. Finds the owner of 393 $(D b) and calls $(D reallocate) for it. If that fails, calls the global 394 $(D reallocate), which allocates a new block and moves memory. 395 */ 396 static if (__traits(hasMember, Allocator, "reallocate")) 397 bool reallocate(ref void[] b, size_t s) 398 { 399 // First attempt to reallocate within the existing node 400 if (!b.ptr) 401 { 402 b = allocate(s); 403 return b.length == s; 404 } 405 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 406 { 407 if (n.owns(b) == Ternary.yes) return n.reallocate(b, s); 408 } 409 // Failed, but we may find new memory in a new node. 410 return .reallocate(this, b, s); 411 } 412 413 /** 414 Defined if $(D Allocator.deallocate) and $(D Allocator.owns) are defined. 415 */ 416 static if (__traits(hasMember, Allocator, "deallocate") 417 && __traits(hasMember, Allocator, "owns")) 418 bool deallocate(void[] b) 419 { 420 if (!b.ptr) return true; 421 assert(allocators.length); 422 assert(owns(b) == Ternary.yes); 423 bool result; 424 for (auto p = &root, n = *p; ; p = &n.next, n = *p) 425 { 426 assert(n); 427 if (n.owns(b) != Ternary.yes) continue; 428 result = n.deallocate(b); 429 // Bring to front 430 if (n != root) 431 { 432 *p = n.next; 433 n.next = root; 434 root = n; 435 } 436 if (n.empty != Ternary.yes) return result; 437 break; 438 } 439 // Hmmm... should we return this allocator back to the wild? Let's 440 // decide if there are TWO empty allocators we can release ONE. This 441 // is to avoid thrashing. 442 // Note that loop starts from the second element. 443 for (auto p = &root.next, n = *p; n; p = &n.next, n = *p) 444 { 445 if (n.unused || n.empty != Ternary.yes) continue; 446 // Used and empty baby, nuke it! 447 n.a.destroy; 448 *p = n.next; 449 n.setUnused; 450 break; 451 } 452 return result; 453 } 454 455 /** 456 Defined only if $(D Allocator.owns) and $(D Allocator.deallocateAll) are 457 defined. 458 */ 459 static if (ouroboros && __traits(hasMember, Allocator, "deallocateAll") 460 && __traits(hasMember, Allocator, "owns")) 461 bool deallocateAll() 462 { 463 Node* special; 464 foreach (ref n; allocators) 465 { 466 if (n.unused) continue; 467 if (n.owns(allocators) == Ternary.yes) 468 { 469 special = &n; 470 continue; 471 } 472 n.a.deallocateAll; 473 n.a.destroy; 474 } 475 assert(special || !allocators.ptr); 476 if (special) 477 { 478 special.deallocate(allocators); 479 } 480 allocators = null; 481 root = null; 482 return true; 483 } 484 485 static if (!ouroboros && __traits(hasMember, Allocator, "deallocateAll") 486 && __traits(hasMember, Allocator, "owns")) 487 bool deallocateAll() 488 { 489 foreach (ref n; allocators) 490 { 491 if (n.unused) continue; 492 n.a.deallocateAll; 493 n.a.destroy; 494 } 495 bkalloc.deallocate(allocators); 496 allocators = null; 497 root = null; 498 return true; 499 } 500 501 /** 502 Returns `Ternary.yes` if no allocators are currently active, 503 `Ternary.no` otherwise. This methods never returns `Ternary.unknown`. 504 */ 505 Ternary empty() const 506 { 507 return Ternary(!allocators.length); 508 } 509 } 510 511 /// Ditto 512 template AllocatorList(alias factoryFunction, 513 BookkeepingAllocator = GCAllocator) 514 { 515 alias A = typeof(factoryFunction(size_t(1))); 516 static assert( 517 // is a template function (including literals) 518 is(typeof({A function(size_t) @system x = factoryFunction!size_t;})) 519 || 520 // or a function (including literals) 521 is(typeof({A function(size_t) @system x = factoryFunction;})) 522 , 523 "Only function names and function literals that take size_t" 524 ~ " and return an allocator are accepted, not " 525 ~ typeof(factoryFunction).stringof 526 ); 527 static struct Factory 528 { 529 A opCall(size_t n) { return factoryFunction(n); } 530 } 531 alias AllocatorList = .AllocatorList!(Factory, BookkeepingAllocator); 532 } 533 534 /// 535 version(Posix) @system unittest 536 { 537 import mir.utility : max; 538 import stdx.allocator.building_blocks.free_list : ContiguousFreeList; 539 import stdx.allocator.building_blocks.null_allocator : NullAllocator; 540 import stdx.allocator.building_blocks.region : Region; 541 import stdx.allocator.building_blocks.segregator : Segregator; 542 import stdx.allocator.gc_allocator : GCAllocator; 543 import stdx.allocator.mmap_allocator : MmapAllocator; 544 545 // Ouroboros allocator list based upon 4MB regions, fetched directly from 546 // mmap. All memory is released upon destruction. 547 alias A1 = AllocatorList!((n) => Region!MmapAllocator(max(n, 1024u * 4096u)), 548 NullAllocator); 549 550 // Allocator list based upon 4MB regions, fetched from the garbage 551 // collector. All memory is released upon destruction. 552 alias A2 = AllocatorList!((n) => Region!GCAllocator(max(n, 1024u * 4096u))); 553 554 // Ouroboros allocator list based upon 4MB regions, fetched from the garbage 555 // collector. Memory is left to the collector. 556 alias A3 = AllocatorList!( 557 (n) => Region!NullAllocator(new ubyte[max(n, 1024u * 4096u)]), 558 NullAllocator); 559 560 // Allocator list that creates one freelist for all objects 561 alias A4 = 562 Segregator!( 563 64, AllocatorList!( 564 (n) => ContiguousFreeList!(NullAllocator, 0, 64)( 565 cast(ubyte[])(GCAllocator.instance.allocate(4096)))), 566 GCAllocator); 567 568 A4 a; 569 auto small = a.allocate(64); 570 assert(small); 571 a.deallocate(small); 572 auto b1 = a.allocate(1024 * 8192); 573 assert(b1 !is null); // still works due to overdimensioning 574 b1 = a.allocate(1024 * 10); 575 assert(b1.length == 1024 * 10); 576 } 577 578 @system unittest 579 { 580 // Create an allocator based upon 4MB regions, fetched from the GC heap. 581 import mir.utility : max; 582 import stdx.allocator.building_blocks.region : Region; 583 AllocatorList!((n) => Region!GCAllocator(new ubyte[max(n, 1024u * 4096u)]), 584 NullAllocator) a; 585 const b1 = a.allocate(1024 * 8192); 586 assert(b1 !is null); // still works due to overdimensioning 587 const b2 = a.allocate(1024 * 10); 588 assert(b2.length == 1024 * 10); 589 a.deallocateAll(); 590 } 591 592 @system unittest 593 { 594 // Create an allocator based upon 4MB regions, fetched from the GC heap. 595 import mir.utility : max; 596 import stdx.allocator.building_blocks.region : Region; 597 AllocatorList!((n) => Region!()(new ubyte[max(n, 1024u * 4096u)])) a; 598 auto b1 = a.allocate(1024 * 8192); 599 assert(b1 !is null); // still works due to overdimensioning 600 b1 = a.allocate(1024 * 10); 601 assert(b1.length == 1024 * 10); 602 a.deallocateAll(); 603 } 604 605 @system unittest 606 { 607 import mir.utility : max; 608 import stdx.allocator.building_blocks.region : Region; 609 import stdx.allocator.internal : Ternary; 610 AllocatorList!((n) => Region!()(new ubyte[max(n, 1024u * 4096u)])) a; 611 auto b1 = a.allocate(1024 * 8192); 612 assert(b1 !is null); 613 b1 = a.allocate(1024 * 10); 614 assert(b1.length == 1024 * 10); 615 a.allocate(1024 * 4095); 616 a.deallocateAll(); 617 assert(a.empty == Ternary.yes); 618 } 619 620 @system unittest 621 { 622 import stdx.allocator.building_blocks.region : Region; 623 enum bs = GCAllocator.alignment; 624 AllocatorList!((n) => Region!GCAllocator(256 * bs)) a; 625 auto b1 = a.allocate(192 * bs); 626 assert(b1.length == 192 * bs); 627 assert(a.allocators.length == 1); 628 auto b2 = a.allocate(64 * bs); 629 assert(b2.length == 64 * bs); 630 assert(a.allocators.length == 1); 631 auto b3 = a.allocate(192 * bs); 632 assert(b3.length == 192 * bs); 633 assert(a.allocators.length == 2); 634 a.deallocate(b1); 635 b1 = a.allocate(64 * bs); 636 assert(b1.length == 64 * bs); 637 assert(a.allocators.length == 2); 638 a.deallocateAll(); 639 }