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