1 // Written in the D programming language. 2 /** 3 Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/allocator_list.d) 4 */ 5 module std.experimental.allocator.building_blocks.allocator_list; 6 7 import core.memory : pageSize; 8 9 import std.experimental.allocator.building_blocks.null_allocator; 10 import std.experimental.allocator.common; 11 import std.experimental.allocator.gc_allocator; 12 13 // Turn this on for debugging 14 // debug = allocator_list; 15 16 /** 17 18 Given an $(LINK2 https://en.wikipedia.org/wiki/Factory_(object-oriented_programming), 19 object factory) of type `Factory` or a factory function 20 `factoryFunction`, and optionally also `BookkeepingAllocator` as a supplemental 21 allocator for bookkeeping, `AllocatorList` creates an allocator that lazily 22 creates as many allocators are needed for satisfying client allocation requests. 23 24 An embedded list builds a most-recently-used strategy: the most recent 25 allocators used in calls to either `allocate`, `owns` (successful calls 26 only), or `deallocate` are tried for new allocations in order of their most 27 recent use. Thus, although core operations take in theory $(BIGOH k) time for 28 `k` allocators in current use, in many workloads the factor is sublinear. 29 Details of the actual strategy may change in future releases. 30 31 `AllocatorList` is primarily intended for coarse-grained handling of 32 allocators, i.e. the number of allocators in the list is expected to be 33 relatively small compared to the number of allocations handled by each 34 allocator. However, the per-allocator overhead is small so using 35 `AllocatorList` with a large number of allocators should be satisfactory as long 36 as the most-recently-used strategy is fast enough for the application. 37 38 `AllocatorList` makes an effort to return allocated memory back when no 39 longer used. It does so by destroying empty allocators. However, in order to 40 avoid thrashing (excessive creation/destruction of allocators under certain use 41 patterns), it keeps unused allocators for a while. 42 43 The shared version of `AllocatorList` is `SharedAllocatorList`, which has 44 identical semantics to its single-threaded version. Both `BookkeepingAllocator` 45 and `Allocator` provided by `factoryFunction` must be shared, in order to 46 ensure corectness. 47 48 Params: 49 factoryFunction = A function or template function (including function literals). 50 New allocators are created by calling `factoryFunction(n)` with strictly 51 positive numbers `n`. Delegates that capture their enviroment are not created 52 amid concerns regarding garbage creation for the environment. When the factory 53 needs state, a `Factory` object should be used. 54 55 BookkeepingAllocator = Allocator used for storing bookkeeping data. The size of 56 bookkeeping data is proportional to the number of allocators. If $(D 57 BookkeepingAllocator) is `NullAllocator`, then `AllocatorList` is 58 "ouroboros-style", i.e. it keeps the bookkeeping data in memory obtained from 59 the allocators themselves. Note that for ouroboros-style management, the size 60 `n` passed to `make` will be occasionally different from the size 61 requested by client code. 62 63 Factory = Type of a factory object that returns new allocators on a need 64 basis. For an object `sweatshop` of type `Factory`, `sweatshop(n)` should 65 return an allocator able to allocate at least `n` bytes (i.e. `Factory` must 66 define `opCall(size_t)` to return an allocator object). Usually the capacity of 67 allocators created should be much larger than `n` such that an allocator can 68 be used for many subsequent allocations. `n` is passed only to ensure the 69 minimum necessary for the next allocation. The factory object is allowed to hold 70 state, which will be stored inside `AllocatorList` as a direct `public` member 71 called `factory`. 72 73 */ 74 struct AllocatorList(Factory, BookkeepingAllocator = GCAllocator) 75 { 76 import core.lifetime : emplace; 77 import std.experimental.allocator.building_blocks.stats_collector 78 : StatsCollector, Options; 79 import std.traits : hasMember; 80 import std.typecons : Ternary; 81 82 private enum ouroboros = is(BookkeepingAllocator == NullAllocator); 83 84 /** 85 Alias for `typeof(Factory()(1))`, i.e. the type of the individual 86 allocators. 87 */ 88 alias Allocator = typeof(Factory.init(1)); 89 // Allocator used internally 90 private alias SAllocator = StatsCollector!(Allocator, Options.bytesUsed); 91 92 private static struct Node 93 { 94 // Allocator in this node 95 SAllocator a; 96 Node* next; 97 98 @disable this(this); 99 100 // Is this node unused? 101 void setUnused() { next = &this; } 102 bool unused() const { return next is &this; } 103 104 // Just forward everything to the allocator 105 alias a this; 106 } 107 108 /** 109 If `BookkeepingAllocator` is not `NullAllocator`, `bkalloc` is 110 defined and accessible. 111 */ 112 113 // State is stored in an array, but it has a list threaded through it by 114 // means of "nextIdx". 115 116 // state 117 static if (!ouroboros) 118 { 119 static if (stateSize!BookkeepingAllocator) BookkeepingAllocator bkalloc; 120 else alias bkalloc = BookkeepingAllocator.instance; 121 } 122 static if (stateSize!Factory) 123 { 124 Factory factory; 125 } 126 private Node[] allocators; 127 private Node* root; 128 129 static if (stateSize!Factory) 130 { 131 private auto make(size_t n) { return factory(n); } 132 } 133 else 134 { 135 private auto make(size_t n) { Factory f; return f(n); } 136 } 137 138 /** 139 Constructs an `AllocatorList` given a factory object. This constructor is 140 defined only if `Factory` has state. 141 */ 142 static if (stateSize!Factory) 143 this(ref Factory plant) 144 { 145 factory = plant; 146 } 147 /// Ditto 148 static if (stateSize!Factory) 149 this(Factory plant) 150 { 151 factory = plant; 152 } 153 154 static if (hasMember!(Allocator, "deallocateAll") 155 && hasMember!(Allocator, "owns")) 156 ~this() 157 { 158 deallocateAll; 159 } 160 161 /** 162 The alignment offered. 163 */ 164 enum uint alignment = Allocator.alignment; 165 166 /** 167 Allocate a block of size `s`. First tries to allocate from the existing 168 list of already-created allocators. If neither can satisfy the request, 169 creates a new allocator by calling `make(s)` and delegates the request 170 to it. However, if the allocation fresh off a newly created allocator 171 fails, subsequent calls to `allocate` will not cause more calls to $(D 172 make). 173 */ 174 void[] allocate(size_t s) 175 { 176 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 177 { 178 auto result = n.allocate(s); 179 if (result.length != s) continue; 180 // Bring to front if not already 181 if (root != n) 182 { 183 *p = n.next; 184 n.next = root; 185 root = n; 186 } 187 return result; 188 } 189 190 // Add a new allocator 191 if (auto a = addAllocator(s)) 192 { 193 auto result = a.allocate(s); 194 assert(owns(result) == Ternary.yes || !result.ptr); 195 return result; 196 } 197 return null; 198 } 199 200 static if (hasMember!(Allocator, "allocateZeroed")) 201 package(std) void[] allocateZeroed()(size_t s) 202 { 203 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 204 { 205 auto result = n.allocateZeroed(s); 206 if (result.length != s) continue; 207 // Bring to front if not already 208 if (root != n) 209 { 210 *p = n.next; 211 n.next = root; 212 root = n; 213 } 214 return result; 215 } 216 217 // Add a new allocator 218 if (auto a = addAllocator(s)) 219 { 220 auto result = a.allocateZeroed(s); 221 assert(owns(result) == Ternary.yes || !result.ptr); 222 return result; 223 } 224 return null; 225 } 226 227 /** 228 Allocate a block of size `s` with alignment `a`. First tries to allocate 229 from the existing list of already-created allocators. If neither can 230 satisfy the request, creates a new allocator by calling `make(s + a - 1)` 231 and delegates the request to it. However, if the allocation fresh off a 232 newly created allocator fails, subsequent calls to `alignedAllocate` 233 will not cause more calls to `make`. 234 */ 235 static if (hasMember!(Allocator, "alignedAllocate")) 236 void[] alignedAllocate(size_t s, uint theAlignment) 237 { 238 import std.algorithm.comparison : max; 239 import core.checkedint : addu; 240 241 if (theAlignment == 0 || s == 0) 242 return null; 243 244 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 245 { 246 auto result = n.alignedAllocate(s, theAlignment); 247 if (result.length != s) continue; 248 // Bring to front if not already 249 if (root != n) 250 { 251 *p = n.next; 252 n.next = root; 253 root = n; 254 } 255 return result; 256 } 257 258 bool overflow = false; 259 size_t maxSize = addu(s - 1, cast(size_t) theAlignment, overflow); 260 assert(!overflow, "Requested size is too large"); 261 if (overflow) 262 return null; 263 264 // Add a new allocator 265 if (auto a = addAllocator(maxSize)) 266 { 267 auto result = a.alignedAllocate(s, theAlignment); 268 assert(owns(result) == Ternary.yes || !result.ptr); 269 return result; 270 } 271 return null; 272 } 273 274 private void moveAllocators(void[] newPlace) 275 { 276 assert(newPlace.ptr.alignedAt(Node.alignof)); 277 assert(newPlace.length % Node.sizeof == 0); 278 auto newAllocators = cast(Node[]) newPlace; 279 assert(allocators.length <= newAllocators.length); 280 281 // Move allocators 282 foreach (i, ref e; allocators) 283 { 284 if (e.unused) 285 { 286 newAllocators[i].setUnused; 287 continue; 288 } 289 import core.stdc.string : memcpy; 290 memcpy(&newAllocators[i].a, &e.a, e.a.sizeof); 291 if (e.next) 292 { 293 newAllocators[i].next = newAllocators.ptr 294 + (e.next - allocators.ptr); 295 } 296 else 297 { 298 newAllocators[i].next = null; 299 } 300 } 301 302 // Mark the unused portion as unused 303 foreach (i; allocators.length .. newAllocators.length) 304 { 305 newAllocators[i].setUnused; 306 } 307 auto toFree = allocators; 308 309 // Change state 310 root = newAllocators.ptr + (root - allocators.ptr); 311 allocators = newAllocators; 312 313 // Free the olden buffer 314 static if (ouroboros) 315 { 316 static if (hasMember!(Allocator, "deallocate") 317 && hasMember!(Allocator, "owns")) 318 deallocate(toFree); 319 } 320 else 321 { 322 bkalloc.deallocate(toFree); 323 } 324 } 325 326 static if (ouroboros) 327 private Node* addAllocator(size_t atLeastBytes) 328 { 329 void[] t = allocators; 330 static if (hasMember!(Allocator, "expand") 331 && hasMember!(Allocator, "owns")) 332 { 333 immutable bool expanded = t && this.expand(t, Node.sizeof); 334 } 335 else 336 { 337 enum expanded = false; 338 } 339 if (expanded) 340 { 341 import core.stdc.string : memcpy; 342 assert(t.length % Node.sizeof == 0); 343 assert(t.ptr.alignedAt(Node.alignof)); 344 allocators = cast(Node[]) t; 345 allocators[$ - 1].setUnused; 346 auto newAlloc = SAllocator(make(atLeastBytes)); 347 memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof); 348 emplace(&newAlloc); 349 } 350 else 351 { 352 immutable toAlloc = (allocators.length + 1) * Node.sizeof 353 + atLeastBytes + 128; 354 auto newAlloc = SAllocator(make(toAlloc)); 355 auto newPlace = newAlloc.allocate( 356 (allocators.length + 1) * Node.sizeof); 357 if (!newPlace) return null; 358 moveAllocators(newPlace); 359 import core.stdc.string : memcpy; 360 memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof); 361 emplace(&newAlloc); 362 assert(allocators[$ - 1].owns(allocators) == Ternary.yes); 363 } 364 // Insert as new root 365 if (root != &allocators[$ - 1]) 366 { 367 allocators[$ - 1].next = root; 368 root = &allocators[$ - 1]; 369 } 370 else 371 { 372 // This is the first one 373 root.next = null; 374 } 375 assert(!root.unused); 376 return root; 377 } 378 379 static if (!ouroboros) 380 private Node* addAllocator(size_t atLeastBytes) 381 { 382 void[] t = allocators; 383 static if (hasMember!(BookkeepingAllocator, "expand")) 384 immutable bool expanded = bkalloc.expand(t, Node.sizeof); 385 else 386 immutable bool expanded = false; 387 if (expanded) 388 { 389 assert(t.length % Node.sizeof == 0); 390 assert(t.ptr.alignedAt(Node.alignof)); 391 allocators = cast(Node[]) t; 392 allocators[$ - 1].setUnused; 393 } 394 else 395 { 396 // Could not expand, create a new block 397 t = bkalloc.allocate((allocators.length + 1) * Node.sizeof); 398 assert(t.length % Node.sizeof == 0); 399 if (!t.ptr) return null; 400 moveAllocators(t); 401 } 402 assert(allocators[$ - 1].unused); 403 auto newAlloc = SAllocator(make(atLeastBytes)); 404 import core.stdc.string : memcpy; 405 memcpy(&allocators[$ - 1].a, &newAlloc, newAlloc.sizeof); 406 emplace(&newAlloc); 407 // Creation succeeded, insert as root 408 if (allocators.length == 1) 409 allocators[$ - 1].next = null; 410 else 411 allocators[$ - 1].next = root; 412 assert(allocators[$ - 1].a.bytesUsed == 0); 413 root = &allocators[$ - 1]; 414 return root; 415 } 416 417 /** 418 Defined only if `Allocator` defines `owns`. Tries each allocator in 419 turn, in most-recently-used order. If the owner is found, it is moved to 420 the front of the list as a side effect under the assumption it will be used 421 soon. 422 423 Returns: `Ternary.yes` if one allocator was found to return `Ternary.yes`, 424 `Ternary.no` if all component allocators returned `Ternary.no`, and 425 `Ternary.unknown` if no allocator returned `Ternary.yes` and at least one 426 returned `Ternary.unknown`. 427 */ 428 static if (hasMember!(Allocator, "owns")) 429 Ternary owns(void[] b) 430 { 431 auto result = Ternary.no; 432 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 433 { 434 immutable t = n.owns(b); 435 if (t != Ternary.yes) 436 { 437 if (t == Ternary.unknown) result = t; 438 continue; 439 } 440 // Move the owner to front, speculating it'll be used 441 if (n != root) 442 { 443 *p = n.next; 444 n.next = root; 445 root = n; 446 } 447 return Ternary.yes; 448 } 449 return result; 450 } 451 452 /** 453 Defined only if `Allocator.expand` is defined. Finds the owner of `b` 454 and calls `expand` for it. The owner is not brought to the head of the 455 list. 456 */ 457 static if (hasMember!(Allocator, "expand") 458 && hasMember!(Allocator, "owns")) 459 bool expand(ref void[] b, size_t delta) 460 { 461 if (!b) return delta == 0; 462 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 463 { 464 if (n.owns(b) == Ternary.yes) return n.expand(b, delta); 465 } 466 return false; 467 } 468 469 /** 470 Defined only if `Allocator.reallocate` is defined. Finds the owner of 471 `b` and calls `reallocate` for it. If that fails, calls the global 472 `reallocate`, which allocates a new block and moves memory. 473 */ 474 static if (hasMember!(Allocator, "reallocate")) 475 bool reallocate(ref void[] b, size_t s) 476 { 477 // First attempt to reallocate within the existing node 478 if (!b.ptr) 479 { 480 b = allocate(s); 481 return b.length == s; 482 } 483 for (auto p = &root, n = *p; n; p = &n.next, n = *p) 484 { 485 if (n.owns(b) == Ternary.yes) return n.reallocate(b, s); 486 } 487 // Failed, but we may find new memory in a new node. 488 return .reallocate(this, b, s); 489 } 490 491 /** 492 Defined if `Allocator.deallocate` and `Allocator.owns` are defined. 493 */ 494 static if (hasMember!(Allocator, "deallocate") 495 && hasMember!(Allocator, "owns")) 496 bool deallocate(void[] b) 497 { 498 if (!b.ptr) return true; 499 assert(allocators.length); 500 assert(owns(b) == Ternary.yes); 501 bool result; 502 for (auto p = &root, n = *p; ; p = &n.next, n = *p) 503 { 504 assert(n); 505 if (n.owns(b) != Ternary.yes) continue; 506 result = n.deallocate(b); 507 // Bring to front 508 if (n != root) 509 { 510 *p = n.next; 511 n.next = root; 512 root = n; 513 } 514 if (n.empty != Ternary.yes) return result; 515 break; 516 } 517 // Hmmm... should we return this allocator back to the wild? Let's 518 // decide if there are TWO empty allocators we can release ONE. This 519 // is to avoid thrashing. 520 // Note that loop starts from the second element. 521 for (auto p = &root.next, n = *p; n; p = &n.next, n = *p) 522 { 523 if (n.unused || n.empty != Ternary.yes) continue; 524 // Used and empty baby, nuke it! 525 n.a.destroy; 526 *p = n.next; 527 n.setUnused; 528 break; 529 } 530 return result; 531 } 532 533 /** 534 Defined only if `Allocator.owns` and `Allocator.deallocateAll` are 535 defined. 536 */ 537 static if (ouroboros && hasMember!(Allocator, "deallocateAll") 538 && hasMember!(Allocator, "owns")) 539 bool deallocateAll() 540 { 541 Node* special; 542 foreach (ref n; allocators) 543 { 544 if (n.unused) continue; 545 if (n.owns(allocators) == Ternary.yes) 546 { 547 special = &n; 548 continue; 549 } 550 n.a.deallocateAll; 551 n.a.destroy; 552 } 553 assert(special || !allocators.ptr); 554 if (special) 555 { 556 static if (stateSize!SAllocator) 557 { 558 import core.stdc.string : memcpy; 559 SAllocator specialCopy; 560 assert(special.a.sizeof == specialCopy.sizeof); 561 memcpy(&specialCopy, &special.a, specialCopy.sizeof); 562 emplace(&special.a); 563 specialCopy.deallocateAll(); 564 } 565 else 566 { 567 special.deallocateAll(); 568 } 569 } 570 allocators = null; 571 root = null; 572 return true; 573 } 574 575 static if (!ouroboros && hasMember!(Allocator, "deallocateAll") 576 && hasMember!(Allocator, "owns")) 577 bool deallocateAll() 578 { 579 foreach (ref n; allocators) 580 { 581 if (n.unused) continue; 582 n.a.deallocateAll; 583 n.a.destroy; 584 } 585 bkalloc.deallocate(allocators); 586 allocators = null; 587 root = null; 588 return true; 589 } 590 591 /** 592 Returns `Ternary.yes` if no allocators are currently active, 593 `Ternary.no` otherwise. This methods never returns `Ternary.unknown`. 594 */ 595 pure nothrow @safe @nogc 596 Ternary empty() const 597 { 598 return Ternary(!allocators.length); 599 } 600 } 601 602 /// Ditto 603 template AllocatorList(alias factoryFunction, 604 BookkeepingAllocator = GCAllocator) 605 { 606 alias A = typeof(factoryFunction(1)); 607 static assert( 608 // is a template function (including literals) 609 is(typeof({A function(size_t) @system x = factoryFunction!size_t;})) 610 || 611 // or a function (including literals) 612 is(typeof({A function(size_t) @system x = factoryFunction;})) 613 , 614 "Only function names and function literals that take size_t" 615 ~ " and return an allocator are accepted, not " 616 ~ typeof(factoryFunction).stringof 617 ); 618 static struct Factory 619 { 620 A opCall(size_t n) { return factoryFunction(n); } 621 } 622 alias AllocatorList = .AllocatorList!(Factory, BookkeepingAllocator); 623 } 624 625 /// 626 version (Posix) @system unittest 627 { 628 import std.algorithm.comparison : max; 629 import std.experimental.allocator.building_blocks.free_list : ContiguousFreeList; 630 import std.experimental.allocator.building_blocks.null_allocator : NullAllocator; 631 import std.experimental.allocator.building_blocks.region : Region; 632 import std.experimental.allocator.building_blocks.segregator : Segregator; 633 import std.experimental.allocator.gc_allocator : GCAllocator; 634 import std.experimental.allocator.mmap_allocator : MmapAllocator; 635 636 // Ouroboros allocator list based upon 4MB regions, fetched directly from 637 // mmap. All memory is released upon destruction. 638 alias A1 = AllocatorList!((n) => Region!MmapAllocator(max(n, 1024 * 4096)), 639 NullAllocator); 640 641 // Allocator list based upon 4MB regions, fetched from the garbage 642 // collector. All memory is released upon destruction. 643 alias A2 = AllocatorList!((n) => Region!GCAllocator(max(n, 1024 * 4096))); 644 645 // Ouroboros allocator list based upon 4MB regions, fetched from the garbage 646 // collector. Memory is left to the collector. 647 alias A3 = AllocatorList!( 648 (n) => Region!NullAllocator(new ubyte[max(n, 1024 * 4096)]), 649 NullAllocator); 650 651 // Allocator list that creates one freelist for all objects 652 alias A4 = 653 Segregator!( 654 64, AllocatorList!( 655 (n) => ContiguousFreeList!(NullAllocator, 0, 64)( 656 cast(ubyte[])(GCAllocator.instance.allocate(4096)))), 657 GCAllocator); 658 659 A4 a; 660 auto small = a.allocate(64); 661 assert(small); 662 a.deallocate(small); 663 auto b1 = a.allocate(1024 * 8192); 664 assert(b1 !is null); // still works due to overdimensioning 665 b1 = a.allocate(1024 * 10); 666 assert(b1.length == 1024 * 10); 667 } 668 669 /// Ditto 670 shared struct SharedAllocatorList(Factory, BookkeepingAllocator = GCAllocator) 671 { 672 import std.typecons : Ternary; 673 import std.traits : hasMember; 674 import core.internal.spinlock : SpinLock; 675 676 private: 677 // Forward all calls to 'impl' and protect them by the lock below 678 AllocatorList!(Factory, BookkeepingAllocator) impl; 679 SpinLock lock = SpinLock(SpinLock.Contention.brief); 680 681 // This could be potentially removed in the future, 682 // should a successor to <https://github.com/dlang/druntime/pull/2156> 683 // or a solution to <https://github.com/dlang/dmd/issues/17128> get merged. 684 static ref T assumeUnshared(T)(ref shared T val) @trusted @nogc pure nothrow 685 { 686 return *cast(T*) &val; 687 } 688 689 // Debug function used for testing 690 version (unittest) 691 auto allocators() 692 { 693 return impl.allocators; 694 } 695 696 // Everything is inherited from the 'AllocatorList' implementation 697 public: 698 699 /* 700 Note: This does not work well with rvalues because it copies them once more. 701 Probably not a problem here because all parameters are cheap. 702 <https://github.com/dlang/phobos/pull/6465/files#r189629862> 703 */ 704 705 /** 706 The alignment offered. 707 */ 708 enum alignment = impl.alignment; 709 710 /** 711 Allocate a block of size `s`. First tries to allocate from the existing 712 list of already-created allocators. If neither can satisfy the request, 713 creates a new allocator by calling `make(s)` and delegates the request 714 to it. However, if the allocation fresh off a newly created allocator 715 fails, subsequent calls to `allocate` will not cause more calls to $(D 716 make). 717 */ 718 static if (hasMember!(typeof(impl), "allocate")) 719 void[] allocate(size_t s) 720 { 721 lock.lock(); 722 scope(exit) lock.unlock(); 723 724 return assumeUnshared(impl).allocate(s); 725 } 726 727 /** 728 Allocate a block of size `s` with alignment `a`. First tries to allocate 729 from the existing list of already-created allocators. If neither can 730 satisfy the request, creates a new allocator by calling `make(s + a - 1)` 731 and delegates the request to it. However, if the allocation fresh off a 732 newly created allocator fails, subsequent calls to `alignedAllocate` 733 will not cause more calls to `make`. 734 */ 735 static if (hasMember!(typeof(impl), "alignedAllocate")) 736 void[] alignedAllocate(size_t s, uint a) 737 { 738 lock.lock(); 739 scope(exit) lock.unlock(); 740 741 return assumeUnshared(impl).alignedAllocate(s, a); 742 } 743 744 /** 745 Defined if `Allocator.deallocate` and `Allocator.owns` are defined. 746 */ 747 static if (hasMember!(typeof(impl), "deallocate")) 748 bool deallocate(void[] b) 749 { 750 lock.lock(); 751 scope(exit) lock.unlock(); 752 753 return assumeUnshared(impl).deallocate(b); 754 } 755 756 /** 757 Defined only if `Allocator` defines `owns`. Tries each allocator in 758 turn, in most-recently-used order. If the owner is found, it is moved to 759 the front of the list as a side effect under the assumption it will be used 760 soon. 761 762 Returns: `Ternary.yes` if one allocator was found to return `Ternary.yes`, 763 `Ternary.no` if all component allocators returned `Ternary.no`, and 764 `Ternary.unknown` if no allocator returned `Ternary.yes` and at least one 765 returned `Ternary.unknown`. 766 */ 767 static if (hasMember!(typeof(impl), "owns")) 768 Ternary owns(void[] b) 769 { 770 lock.lock(); 771 scope(exit) lock.unlock(); 772 773 return assumeUnshared(impl).owns(b); 774 } 775 776 /** 777 Defined only if `Allocator.expand` is defined. Finds the owner of `b` 778 and calls `expand` for it. The owner is not brought to the head of the 779 list. 780 */ 781 static if (hasMember!(typeof(impl), "expand")) 782 bool expand(ref void[] b, size_t delta) 783 { 784 lock.lock(); 785 scope(exit) lock.unlock(); 786 787 return assumeUnshared(impl).expand(b, delta); 788 } 789 790 /** 791 Defined only if `Allocator.reallocate` is defined. Finds the owner of 792 `b` and calls `reallocate` for it. If that fails, calls the global 793 `reallocate`, which allocates a new block and moves memory. 794 */ 795 static if (hasMember!(typeof(impl), "reallocate")) 796 bool reallocate(ref void[] b, size_t s) 797 { 798 lock.lock(); 799 scope(exit) lock.unlock(); 800 801 return assumeUnshared(impl).reallocate(b, s); 802 } 803 804 /** 805 Defined only if `Allocator.owns` and `Allocator.deallocateAll` are 806 defined. 807 */ 808 static if (hasMember!(typeof(impl), "deallocateAll")) 809 bool deallocateAll() 810 { 811 lock.lock(); 812 scope(exit) lock.unlock(); 813 814 return assumeUnshared(impl).deallocateAll(); 815 } 816 817 /** 818 Returns `Ternary.yes` if no allocators are currently active, 819 `Ternary.no` otherwise. This methods never returns `Ternary.unknown`. 820 */ 821 static if (hasMember!(typeof(impl), "empty")) 822 Ternary empty() 823 { 824 lock.lock(); 825 scope(exit) lock.unlock(); 826 827 return assumeUnshared(impl).empty(); 828 } 829 } 830 831 /// Ditto 832 template SharedAllocatorList(alias factoryFunction, 833 BookkeepingAllocator = GCAllocator) 834 { 835 alias A = typeof(factoryFunction(1)); 836 static assert( 837 // is a template function (including literals) 838 is(typeof({A function(size_t) @system x = factoryFunction!size_t;})) 839 || 840 // or a function (including literals) 841 is(typeof({A function(size_t) @system x = factoryFunction;})) 842 , 843 "Only function names and function literals that take size_t" 844 ~ " and return an allocator are accepted, not " 845 ~ typeof(factoryFunction).stringof 846 ); 847 static struct Factory 848 { 849 A opCall(size_t n) { return factoryFunction(n); } 850 } 851 alias SharedAllocatorList = .SharedAllocatorList!(Factory, BookkeepingAllocator); 852 } 853 854 @system unittest 855 { 856 import std.algorithm.comparison : max; 857 import std.experimental.allocator.building_blocks.region : Region, SharedRegion; 858 859 static void testAlloc(Allocator)(ref Allocator a) 860 { 861 const b1 = a.allocate(1024 * 8192); 862 assert(b1 !is null); // still works due to overdimensioning 863 const b2 = a.allocate(1024 * 10); 864 assert(b2.length == 1024 * 10); 865 a.deallocateAll(); 866 } 867 868 // Create an allocator based upon 4MB regions, fetched from the GC heap. 869 AllocatorList!((n) => Region!GCAllocator(new ubyte[max(n, 1024 * 4096)]), 870 NullAllocator) reg1; 871 872 SharedAllocatorList!((n) => SharedRegion!GCAllocator(new ubyte[max(n, 1024 * 4096)]), 873 NullAllocator) reg2; 874 875 testAlloc(reg1); 876 testAlloc(reg2); 877 } 878 879 @system unittest 880 { 881 import std.algorithm.comparison : max; 882 import std.experimental.allocator.building_blocks.region : BorrowedRegion, SharedBorrowedRegion; 883 884 static void testAlloc(Allocator)(ref Allocator a) 885 { 886 auto b1 = a.alignedAllocate(1024 * 8192, 1024); 887 assert(b1 !is null); // still works due to overdimensioning 888 assert(b1.length == 1024 * 8192); 889 assert(b1.ptr.alignedAt(1024)); 890 assert(a.allocators.length == 1); 891 892 b1 = a.alignedAllocate(0, 1024); 893 assert(b1.length == 0); 894 assert(a.allocators.length == 1); 895 896 b1 = a.allocate(1024 * 10); 897 assert(b1.length == 1024 * 10); 898 899 assert(a.reallocate(b1, 1024)); 900 assert(b1.length == 1024); 901 902 a.deallocateAll(); 903 } 904 905 // Create an allocator based upon 4MB regions, fetched from the GC heap. 906 AllocatorList!((n) => BorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a1; 907 SharedAllocatorList!((n) => SharedBorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a2; 908 909 testAlloc(a1); 910 testAlloc(a2); 911 } 912 913 @system unittest 914 { 915 import core.exception : AssertError; 916 import std.exception : assertThrown; 917 import std.algorithm.comparison : max; 918 import std.experimental.allocator.building_blocks.region : BorrowedRegion, SharedBorrowedRegion; 919 920 static void testAlloc(Allocator)(ref Allocator a) 921 { 922 auto b1 = a.alignedAllocate(0, 1); 923 assert(b1 is null); 924 925 b1 = a.alignedAllocate(1, 0); 926 assert(b1 is null); 927 928 b1 = a.alignedAllocate(0, 0); 929 assert(b1 is null); 930 931 assertThrown!AssertError(a.alignedAllocate(size_t.max, 1024)); 932 933 // FIXME: This special-casing might note be necessary. 934 // At the moment though, this call would take potentially forever 935 // for the `SharedAllocatorList` from below. 936 static if (!is(Allocator == shared)) 937 { 938 a.deallocateAll(); 939 } 940 } 941 942 // Create an allocator based upon 4MB regions, fetched from the GC heap. 943 AllocatorList!((n) => BorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a1; 944 SharedAllocatorList!((n) => SharedBorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a2; 945 946 testAlloc(a1); 947 testAlloc(a2); 948 } 949 950 @system unittest 951 { 952 import std.typecons : Ternary; 953 954 // Create an allocator based upon 4MB regions, fetched from the GC heap. 955 import std.algorithm.comparison : max; 956 import std.experimental.allocator.building_blocks.region : BorrowedRegion, SharedBorrowedRegion; 957 958 static void testAlloc(Allocator)(ref Allocator a) 959 { 960 auto b0 = a.alignedAllocate(1, 1024); 961 assert(b0.length == 1); 962 assert(b0.ptr.alignedAt(1024)); 963 assert(a.allocators.length == 1); 964 965 auto b1 = a.alignedAllocate(1024 * 4096, 1024); 966 assert(b1.length == 1024 * 4096); 967 assert(b1.ptr.alignedAt(1024)); 968 assert(a.allocators.length == 2); 969 970 auto b2 = a.alignedAllocate(1024, 128); 971 assert(b2.length == 1024); 972 assert(b2.ptr.alignedAt(128)); 973 assert(a.allocators.length == 2); 974 975 auto b3 = a.allocate(1024); 976 assert(b3.length == 1024); 977 assert(a.allocators.length == 2); 978 979 auto b4 = a.allocate(1024 * 4096); 980 assert(b4.length == 1024 * 4096); 981 assert(a.allocators.length == 3); 982 983 static if (!is(Allocator == shared)) 984 { 985 assert(a.root.empty == Ternary.no); 986 assert(a.deallocate(b4)); 987 assert(a.root.empty == Ternary.yes); 988 989 assert(a.deallocate(b1)); 990 } 991 992 a.deallocateAll(); 993 } 994 995 AllocatorList!((n) => BorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a1; 996 SharedAllocatorList!((n) => SharedBorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a2; 997 998 testAlloc(a1); 999 testAlloc(a2); 1000 } 1001 1002 @system unittest 1003 { 1004 import std.algorithm.comparison : max; 1005 import std.experimental.allocator.building_blocks.region : BorrowedRegion, SharedBorrowedRegion; 1006 1007 static void testAlloc(Allocator)(ref Allocator a) 1008 { 1009 auto b1 = a.allocate(1024 * 8192); 1010 assert(b1 !is null); // still works due to overdimensioning 1011 b1 = a.allocate(1024 * 10); 1012 assert(b1.length == 1024 * 10); 1013 assert(a.reallocate(b1, 1024)); 1014 assert(b1.length == 1024); 1015 a.deallocateAll(); 1016 } 1017 1018 // Create an allocator based upon 4MB regions, fetched from the GC heap. 1019 AllocatorList!((n) => BorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a1; 1020 SharedAllocatorList!((n) => SharedBorrowedRegion!()(new ubyte[max(n, 1024 * 4096)])) a2; 1021 1022 testAlloc(a1); 1023 testAlloc(a2); 1024 } 1025 1026 @system unittest 1027 { 1028 import std.algorithm.comparison : max; 1029 import std.experimental.allocator.building_blocks.region : BorrowedRegion; 1030 import std.experimental.allocator.mallocator : Mallocator; 1031 import std.typecons : Ternary; 1032 AllocatorList!((n) => BorrowedRegion!()(new ubyte[max(n, 1024 * 4096)]), Mallocator) a; 1033 auto b1 = a.allocate(1024 * 8192); 1034 assert(b1 !is null); 1035 b1 = a.allocate(1024 * 10); 1036 assert(b1.length == 1024 * 10); 1037 assert((() pure nothrow @safe @nogc => a.expand(b1, 10))()); 1038 assert(b1.length == 1025 * 10); 1039 a.allocate(1024 * 4095); 1040 assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.no); 1041 // Ensure deallocateAll infers from parent 1042 assert((() nothrow @nogc => a.deallocateAll())()); 1043 assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.yes); 1044 } 1045 1046 @system unittest 1047 { 1048 import std.experimental.allocator.building_blocks.region : Region, SharedRegion; 1049 enum bs = GCAllocator.alignment; 1050 1051 static void testAlloc(Allocator)(ref Allocator a) 1052 { 1053 auto b1 = a.allocate(192 * bs); 1054 assert(b1.length == 192 * bs); 1055 assert(a.allocators.length == 1); 1056 auto b2 = a.allocate(64 * bs); 1057 assert(b2.length == 64 * bs); 1058 assert(a.allocators.length == 1); 1059 auto b3 = a.allocate(192 * bs); 1060 assert(b3.length == 192 * bs); 1061 assert(a.allocators.length == 2); 1062 // Ensure deallocate inherits from parent allocators 1063 () nothrow @nogc { a.deallocate(b1); }(); 1064 b1 = a.allocate(64 * bs); 1065 assert(b1.length == 64 * bs); 1066 assert(a.allocators.length == 2); 1067 a.deallocateAll(); 1068 } 1069 1070 AllocatorList!((n) => Region!GCAllocator(256 * bs)) a1; 1071 SharedAllocatorList!((n) => SharedRegion!GCAllocator(256 * bs)) a2; 1072 1073 testAlloc(a1); 1074 testAlloc(a2); 1075 } 1076 1077 @system unittest 1078 { 1079 import std.experimental.allocator.building_blocks.ascending_page_allocator : AscendingPageAllocator; 1080 import std.experimental.allocator.mallocator : Mallocator; 1081 import std.algorithm.comparison : max; 1082 import std.typecons : Ternary; 1083 1084 static void testrw(void[] b) 1085 { 1086 ubyte* buf = cast(ubyte*) b.ptr; 1087 for (int i = 0; i < b.length; i += pageSize) 1088 { 1089 buf[i] = cast(ubyte) (i % 256); 1090 assert(buf[i] == cast(ubyte) (i % 256)); 1091 } 1092 } 1093 1094 enum numPages = 2; 1095 AllocatorList!((n) => AscendingPageAllocator(max(n, numPages * pageSize)), Mallocator) a; 1096 1097 void[] b1 = a.allocate(1); 1098 assert(b1.length == 1); 1099 b1 = a.allocate(2); 1100 assert(b1.length == 2); 1101 testrw(b1); 1102 assert(a.root.a.parent.getAvailableSize() == 0); 1103 1104 void[] b2 = a.allocate((numPages + 1) * pageSize); 1105 assert(b2.length == (numPages + 1) * pageSize); 1106 testrw(b2); 1107 1108 void[] b3 = a.allocate(3); 1109 assert(b3.length == 3); 1110 testrw(b3); 1111 1112 void[] b4 = a.allocate(0); 1113 assert(b4.length == 0); 1114 1115 assert(a.allocators.length == 3); 1116 assert(a.owns(b1) == Ternary.yes); 1117 assert(a.owns(b2) == Ternary.yes); 1118 assert(a.owns(b3) == Ternary.yes); 1119 1120 assert(a.expand(b1, pageSize - b1.length)); 1121 assert(b1.length == pageSize); 1122 assert(!a.expand(b1, 1)); 1123 assert(!a.expand(b2, 1)); 1124 1125 testrw(b1); 1126 testrw(b2); 1127 testrw(b3); 1128 1129 assert(a.deallocate(b1)); 1130 assert(a.deallocate(b2)); 1131 1132 assert(a.deallocateAll()); 1133 } 1134 1135 @system unittest 1136 { 1137 import std.experimental.allocator.building_blocks.ascending_page_allocator : 1138 AscendingPageAllocator, SharedAscendingPageAllocator; 1139 import std.experimental.allocator.mallocator : Mallocator; 1140 import std.algorithm.comparison : max; 1141 import std.typecons : Ternary; 1142 1143 enum numPages = 2; 1144 1145 static void testrw(void[] b) 1146 { 1147 ubyte* buf = cast(ubyte*) b.ptr; 1148 for (int i = 0; i < b.length; i += pageSize) 1149 { 1150 buf[i] = cast(ubyte) (i % 256); 1151 assert(buf[i] == cast(ubyte) (i % 256)); 1152 } 1153 } 1154 1155 static void testAlloc(Allocator)(ref Allocator a) 1156 { 1157 void[] b1 = a.allocate(1); 1158 assert(b1.length == 1); 1159 b1 = a.allocate(2); 1160 assert(b1.length == 2); 1161 testrw(b1); 1162 1163 void[] b2 = a.allocate((numPages + 1) * pageSize); 1164 assert(b2.length == (numPages + 1) * pageSize); 1165 testrw(b2); 1166 1167 void[] b3 = a.allocate(3); 1168 assert(b3.length == 3); 1169 testrw(b3); 1170 1171 void[] b4 = a.allocate(0); 1172 assert(b4.length == 0); 1173 1174 assert(a.allocators.length == 3); 1175 assert(a.owns(b1) == Ternary.yes); 1176 assert(a.owns(b2) == Ternary.yes); 1177 assert(a.owns(b3) == Ternary.yes); 1178 1179 assert(a.expand(b1, pageSize - b1.length)); 1180 assert(b1.length == pageSize); 1181 assert(!a.expand(b1, 1)); 1182 assert(!a.expand(b2, 1)); 1183 1184 testrw(b1); 1185 testrw(b2); 1186 testrw(b3); 1187 1188 assert(a.deallocate(b1)); 1189 assert(a.deallocate(b2)); 1190 1191 const alignment = cast(uint) (70 * pageSize); 1192 b3 = a.alignedAllocate(70 * pageSize, alignment); 1193 assert(b3.length == 70 * pageSize); 1194 assert(b3.ptr.alignedAt(alignment)); 1195 testrw(b3); 1196 assert(a.allocators.length == 4); 1197 assert(a.deallocate(b3)); 1198 1199 1200 assert(a.deallocateAll()); 1201 } 1202 1203 AllocatorList!((n) => AscendingPageAllocator(max(n, numPages * pageSize)), NullAllocator) a1; 1204 SharedAllocatorList!((n) => SharedAscendingPageAllocator(max(n, numPages * pageSize)), NullAllocator) a2; 1205 } 1206 1207 @system unittest 1208 { 1209 import std.experimental.allocator.building_blocks.ascending_page_allocator : AscendingPageAllocator; 1210 import std.experimental.allocator.mallocator : Mallocator; 1211 import std.algorithm.comparison : max; 1212 import std.typecons : Ternary; 1213 1214 static void testrw(void[] b) 1215 { 1216 ubyte* buf = cast(ubyte*) b.ptr; 1217 for (int i = 0; i < b.length; i += pageSize) 1218 { 1219 buf[i] = cast(ubyte) (i % 256); 1220 assert(buf[i] == cast(ubyte) (i % 256)); 1221 } 1222 } 1223 1224 enum numPages = 5; 1225 AllocatorList!((n) => AscendingPageAllocator(max(n, numPages * pageSize)), NullAllocator) a; 1226 const alignment = cast(uint) (2 * pageSize); 1227 auto b = a.alignedAllocate(1, alignment); 1228 assert(b.length == 1); 1229 assert(a.expand(b, pageSize - 1)); 1230 assert(b.ptr.alignedAt(alignment)); 1231 assert(b.length == pageSize); 1232 1233 b = a.allocate(pageSize); 1234 assert(b.length == pageSize); 1235 assert(a.allocators.length == 1); 1236 1237 assert(a.allocate(pageSize * 5).length == pageSize * 5); 1238 assert(a.allocators.length == 2); 1239 1240 assert(a.deallocateAll()); 1241 } 1242 1243 @system unittest 1244 { 1245 import std.experimental.allocator.building_blocks.ascending_page_allocator : AscendingPageAllocator; 1246 import std.algorithm.comparison : max; 1247 1248 enum maxIter = 100; 1249 enum numPages = 10; 1250 const chunkSize = pageSize / 8; 1251 1252 AllocatorList!((n) => AscendingPageAllocator(max(n, numPages * pageSize)), NullAllocator) a; 1253 foreach (i; 0 .. maxIter) 1254 { 1255 auto b1 = a.allocate(chunkSize); 1256 assert(b1.length == chunkSize); 1257 1258 assert(a.deallocate(b1)); 1259 } 1260 1261 assert(a.deallocateAll()); 1262 } 1263 1264 @system unittest 1265 { 1266 import std.experimental.allocator.building_blocks.ascending_page_allocator : AscendingPageAllocator; 1267 import std.experimental.allocator.mallocator : Mallocator; 1268 import std.algorithm.comparison : max; 1269 import std.typecons : Ternary; 1270 1271 static void testrw(void[] b) 1272 { 1273 ubyte* buf = cast(ubyte*) b.ptr; 1274 for (int i = 0; i < b.length; i += pageSize) 1275 { 1276 buf[i] = cast(ubyte) (i % 256); 1277 assert(buf[i] == cast(ubyte) (i % 256)); 1278 } 1279 } 1280 1281 enum numPages = 5; 1282 AllocatorList!((n) => AscendingPageAllocator(max(n, numPages * pageSize)), NullAllocator) a; 1283 auto b = a.alignedAllocate(1, cast(uint) (pageSize * 2)); 1284 assert(b.length == 1); 1285 assert(a.expand(b, pageSize - 1)); 1286 assert(b.ptr.alignedAt(cast(uint) (pageSize * 2))); 1287 assert(b.length == pageSize); 1288 1289 b = a.allocate(pageSize); 1290 assert(b.length == pageSize); 1291 assert(a.allocators.length == 1); 1292 1293 assert(a.allocate(pageSize * 5).length == pageSize * 5); 1294 assert(a.allocators.length == 2); 1295 1296 assert(a.deallocateAll()); 1297 } 1298 1299 @system unittest 1300 { 1301 import std.experimental.allocator.building_blocks.region : SharedRegion; 1302 import core.thread : ThreadGroup; 1303 import std.algorithm.comparison : max; 1304 1305 enum numThreads = 10; 1306 SharedAllocatorList!((n) => SharedRegion!(GCAllocator)(new ubyte[max(n, 1024)])) a; 1307 1308 void fun() 1309 { 1310 void[] b1 = a.allocate(1024); 1311 assert(b1.length == 1024); 1312 1313 void[] b2 = a.alignedAllocate(1024, 1024); 1314 assert(b2.length == 1024); 1315 assert(b2.ptr.alignedAt(1024)); 1316 1317 assert(a.deallocate(b1)); 1318 assert(a.deallocate(b2)); 1319 } 1320 1321 auto tg = new ThreadGroup; 1322 foreach (i; 0 .. numThreads) 1323 { 1324 tg.create(&fun); 1325 } 1326 tg.joinAll(); 1327 1328 assert(a.deallocateAll()); 1329 } 1330 1331 @system unittest 1332 { 1333 import std.experimental.allocator.mallocator : Mallocator; 1334 import std.experimental.allocator.building_blocks.ascending_page_allocator : SharedAscendingPageAllocator; 1335 import core.thread : ThreadGroup; 1336 import std.algorithm.comparison : max; 1337 1338 enum numThreads = 100; 1339 enum numPages = 10; 1340 SharedAllocatorList!((n) => SharedAscendingPageAllocator(max(n, pageSize * numPages)), Mallocator) a; 1341 1342 void fun() 1343 { 1344 void[] b1 = a.allocate(512); 1345 assert(b1.length == 512); 1346 assert(a.expand(b1, 512)); 1347 assert(b1.length == 1024); 1348 1349 void[] b2 = a.alignedAllocate(1024, 4096); 1350 assert(b2.length == 1024); 1351 assert(b2.ptr.alignedAt(1024)); 1352 1353 assert(a.deallocate(b1)); 1354 assert(a.deallocate(b2)); 1355 } 1356 1357 auto tg = new ThreadGroup; 1358 foreach (i; 0 .. numThreads) 1359 { 1360 tg.create(&fun); 1361 } 1362 tg.joinAll(); 1363 1364 assert(a.deallocateAll()); 1365 }