1 /** 2 * Unrolled Linked List. 3 * Copyright: © 2015 Economic Modeling Specialists, Intl. 4 * Authors: Brian Schott 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 */ 7 8 module containers.unrolledlist; 9 10 private import core.lifetime : move; 11 private import containers.internal.node : shouldAddGCRange; 12 private import std.experimental.allocator.mallocator : Mallocator; 13 14 version (X86_64) 15 version (LDC) 16 version = LDC_64; 17 18 /** 19 * Unrolled Linked List. 20 * 21 * Nodes are (by default) sized to fit within a 64-byte cache line. The number 22 * of items stored per node can be read from the $(B nodeCapacity) field. 23 * See_also: $(LINK http://en.wikipedia.org/wiki/Unrolled_linked_list) 24 * Params: 25 * T = the element type 26 * Allocator = the allocator to use. Defaults to `Mallocator`. 27 * supportGC = true to ensure that the GC scans the nodes of the unrolled 28 * list, false if you are sure that no references to GC-managed memory 29 * will be stored in this container. 30 * cacheLineSize = Nodes will be sized to fit within this number of bytes. 31 */ 32 struct UnrolledList(T, Allocator = Mallocator, 33 bool supportGC = shouldAddGCRange!T, size_t cacheLineSize = 64) 34 { 35 this(this) @disable; 36 37 private import std.experimental.allocator.common : stateSize; 38 39 static if (stateSize!Allocator != 0) 40 { 41 /// No default construction if an allocator must be provided. 42 this() @disable; 43 44 /** 45 * Use the given `allocator` for allocations. 46 */ 47 this(Allocator allocator) 48 in 49 { 50 static if (is(typeof(allocator is null))) 51 assert(allocator !is null, "Allocator must not be null"); 52 } 53 do 54 { 55 this.allocator = allocator; 56 } 57 } 58 59 ~this() nothrow 60 { 61 scope (failure) assert(false, "UnrolledList destructor threw an exception"); 62 clear(); 63 } 64 65 /** 66 * Removes all items from the list 67 */ 68 void clear() 69 { 70 Node* previous; 71 Node* current = _front; 72 while (current !is null) 73 { 74 previous = current; 75 current = current.next; 76 77 static if (useGC) 78 { 79 import core.memory: GC; 80 GC.removeRange(previous); 81 } 82 allocator.dispose(previous); 83 } 84 _length = 0; 85 _front = null; 86 _back = null; 87 } 88 89 /** 90 * Inserts the given item into the end of the list. 91 * 92 * Returns: a pointer to the inserted item. 93 */ 94 T* insertBack(T item) 95 { 96 ContainerStorageType!T* result; 97 if (_back is null) 98 { 99 assert (_front is null, "front/back nullness mismatch"); 100 _back = allocateNode(move(mutable(item))); 101 _front = _back; 102 result = &_back.items[0]; 103 } 104 else 105 { 106 size_t index = _back.nextAvailableIndex(); 107 if (index >= nodeCapacity) 108 { 109 Node* n = allocateNode(move(mutable(item))); 110 n.prev = _back; 111 _back.next = n; 112 _back = n; 113 index = 0; 114 result = &n.items[0]; 115 } 116 else 117 { 118 _back.items[index] = move(mutable(item)); 119 _back.markUsed(index); 120 result = &_back.items[index]; 121 } 122 } 123 _length++; 124 assert (_back.registry <= fullBitPattern, "Overflow"); 125 return cast(T*) result; 126 } 127 128 /** 129 * Inserts the given range into the end of the list 130 */ 131 void insertBack(R)(auto ref R range) 132 { 133 foreach (ref r; range) 134 insertBack(r); 135 } 136 137 /// ditto 138 T* opOpAssign(string op)(T item) if (op == "~") 139 { 140 return insertBack(item); 141 } 142 143 /// ditto 144 alias put = insertBack; 145 146 /// ditto 147 alias insert = insertBack; 148 149 /** 150 * Inserts the given item in the frontmost available cell, which may put the 151 * item anywhere in the list as removal may leave gaps in list nodes. Use 152 * this only if the order of elements is not important. 153 * 154 * Returns: a pointer to the inserted item. 155 */ 156 T* insertAnywhere(T item) @trusted 157 { 158 Node* n = _front; 159 while (n !is null) 160 { 161 size_t i = n.nextAvailableIndex(); 162 if (i >= nodeCapacity) 163 { 164 if (n.next is null) 165 { 166 assert (n is _back, "Wrong _back"); 167 break; 168 } 169 n = n.next; 170 continue; 171 } 172 n.items[i] = move(mutable(item)); 173 n.markUsed(i); 174 _length++; 175 assert (n.registry <= fullBitPattern, "Overflow"); 176 return cast(T*) &n.items[i]; 177 } 178 n = allocateNode(move(mutable(item))); 179 _length++; 180 auto retVal = cast(T*) &n.items[0]; 181 if (_front is null) 182 { 183 assert(_back is null, "front/back nullness mismatch"); 184 _front = n; 185 } 186 else 187 { 188 n.prev = _back; 189 _back.next = n; 190 } 191 _back = n; 192 assert (_back.registry <= fullBitPattern, "Overflow"); 193 return retVal; 194 } 195 196 /// Returns: the length of the list 197 size_t length() const nothrow pure @property @safe @nogc 198 { 199 return _length; 200 } 201 202 /// Returns: true if the list is empty 203 bool empty() const nothrow pure @property @safe @nogc 204 { 205 return _length == 0; 206 } 207 208 /** 209 * Removes the first instance of the given item from the list. 210 * 211 * Returns: true if something was removed. 212 */ 213 bool remove(ref const T item) 214 { 215 if (_front is null) 216 return false; 217 bool retVal; 218 loop: for (Node* n = _front; n !is null; n = n.next) 219 { 220 foreach (i; 0 .. nodeCapacity) 221 { 222 if (!n.isFree(i) && n.items[i] == item) 223 { 224 n.markUnused(i); 225 --_length; 226 retVal = true; 227 if (n.registry == 0) 228 deallocateNode(n); 229 else if (shouldMerge(n, n.next)) 230 mergeNodes(n, n.next); 231 else if (shouldMerge(n.prev, n)) 232 mergeNodes(n.prev, n); 233 break loop; 234 } 235 } 236 } 237 return retVal; 238 } 239 bool remove(const T item) { return remove(item); } 240 241 /// Pops the front item off of the list 242 void popFront() 243 { 244 moveFront(); 245 assert (_front is null || _front.registry != 0, "Node is non-null but empty"); 246 } 247 248 /// Pops the front item off of the list and returns it 249 T moveFront() 250 in 251 { 252 assert (!empty(), "Accessing .moveFront of empty UnrolledList"); 253 assert (_front.registry != 0, "Empty node"); 254 } 255 do 256 { 257 version (LDC_64) 258 { 259 import ldc.intrinsics : llvm_cttz; 260 size_t index = llvm_cttz(_front.registry, true); 261 } 262 else 263 { 264 import containers.internal.backwards : bsf; 265 size_t index = bsf(_front.registry); 266 } 267 T r = move(_front.items[index]); 268 _front.markUnused(index); 269 _length--; 270 if (_front.registry == 0) 271 { 272 auto f = _front; 273 if (_front.next !is null) 274 _front.next.prev = null; 275 assert (_front.next !is _front, "Infinite loop"); 276 _front = _front.next; 277 if (_front is null) 278 _back = null; 279 else 280 assert (_front.registry <= fullBitPattern, "Overflow"); 281 deallocateNode(f); 282 return r; 283 } 284 if (shouldMerge(_front, _front.next)) 285 mergeNodes(_front, _front.next); 286 return r; 287 } 288 289 debug (EMSI_CONTAINERS) invariant 290 { 291 import std.string: format; 292 assert (_front is null || _front.registry != 0, format("%x, %b", _front, _front.registry)); 293 assert (_front !is null || _back is null, "_front/_back nullness mismatch"); 294 if (_front !is null) 295 { 296 const(Node)* c = _front; 297 while (c.next !is null) 298 c = c.next; 299 assert(c is _back, "_back pointer is wrong"); 300 } 301 } 302 303 /** 304 * Time complexity is O(1) 305 * Returns: the item at the front of the list 306 */ 307 ref inout(T) front() inout nothrow @property 308 in 309 { 310 assert (!empty, "Accessing .front of empty UnrolledList"); 311 assert (_front.registry != 0, "Empty node"); 312 } 313 do 314 { 315 version (LDC_64) 316 { 317 import ldc.intrinsics : llvm_cttz; 318 immutable index = llvm_cttz(_front.registry, true); 319 } 320 else 321 { 322 import containers.internal.backwards : bsf; 323 immutable index = bsf(_front.registry); 324 } 325 return *(cast(typeof(return)*) &_front.items[index]); 326 } 327 328 /** 329 * Time complexity is O(nodeCapacity), where the nodeCapacity 330 * is the number of items in a single list node. It is a constant 331 * related to the cache line size. 332 * Returns: the item at the back of the list 333 */ 334 ref inout(T) back() inout nothrow @property 335 in 336 { 337 assert (!empty, "Accessing .back of empty UnrolledList"); 338 assert (!_back.empty, "Empty node"); 339 } 340 do 341 { 342 size_t i = nodeCapacity - 1; 343 while (_back.isFree(i)) 344 i--; 345 return *(cast(typeof(return)*) &_back.items[i]); 346 } 347 348 /// Pops the back item off of the list. 349 void popBack() 350 { 351 moveBack(); 352 } 353 354 /// Removes an item from the back of the list and returns it. 355 T moveBack() 356 in 357 { 358 assert (!empty, "Accessing .moveBack of empty UnrolledList"); 359 assert (!_back.empty, "Empty node"); 360 } 361 do 362 { 363 size_t i = nodeCapacity - 1; 364 while (_back.isFree(i)) 365 { 366 if (i == 0) 367 break; 368 else 369 i--; 370 } 371 assert (!_back.isFree(i), "Empty node"); 372 T item = move(_back.items[i]); 373 _back.markUnused(i); 374 _length--; 375 if (_back.registry == 0) 376 { 377 deallocateNode(_back); 378 return item; 379 } 380 else if (shouldMerge(_back.prev, _back)) 381 mergeNodes(_back.prev, _back); 382 return item; 383 } 384 385 /// Returns: a range over the list 386 auto opSlice(this This)() const nothrow pure @nogc @trusted 387 { 388 return Range!(This)(_front); 389 } 390 391 static struct Range(ThisT) 392 { 393 @disable this(); 394 395 this(inout(Node)* current) 396 { 397 import std.format:format; 398 399 this.current = current; 400 if (current !is null) 401 { 402 version(LDC_64) 403 { 404 import ldc.intrinsics : llvm_cttz; 405 index = llvm_cttz(current.registry, true); 406 } 407 else 408 { 409 import containers.internal.backwards : bsf; 410 index = bsf(current.registry); 411 } 412 413 assert (index < nodeCapacity); 414 } 415 else 416 current = null; 417 } 418 419 ref ET front() const nothrow @property @trusted @nogc 420 { 421 return *(cast(ET*) ¤t.items[index]); 422 //return cast(T) current.items[index]; 423 } 424 425 void popFront() nothrow pure @safe @nogc 426 { 427 index++; 428 while (true) 429 { 430 431 if (index >= nodeCapacity) 432 { 433 current = current.next; 434 if (current is null) 435 return; 436 index = 0; 437 } 438 else 439 { 440 if (current.isFree(index)) 441 index++; 442 else 443 return; 444 } 445 } 446 } 447 448 bool empty() const nothrow pure @property @safe @nogc 449 { 450 return current is null; 451 } 452 453 Range save() const nothrow pure @property @safe @nogc 454 { 455 return this; 456 } 457 458 private: 459 460 alias ET = ContainerElementType!(ThisT, T); 461 const(Node)* current; 462 size_t index; 463 } 464 465 private: 466 467 import std.experimental.allocator: make, dispose; 468 import containers.internal.node : FatNodeInfo, shouldAddGCRange, 469 fullBits, shouldNullSlot; 470 import containers.internal.storage_type : ContainerStorageType; 471 import containers.internal.element_type : ContainerElementType; 472 import containers.internal.mixins : AllocatorState; 473 474 alias N = FatNodeInfo!(T.sizeof, 2, cacheLineSize); 475 enum size_t nodeCapacity = N[0]; 476 alias BookkeepingType = N[1]; 477 enum fullBitPattern = fullBits!(BookkeepingType, nodeCapacity); 478 enum bool useGC = supportGC && shouldAddGCRange!T; 479 480 static ref ContainerStorageType!T mutable(ref T value) { return *cast(ContainerStorageType!T*)&value; } 481 482 Node* _back; 483 Node* _front; 484 size_t _length; 485 mixin AllocatorState!Allocator; 486 487 Node* allocateNode(T item) 488 { 489 Node* n = make!Node(allocator); 490 static if (useGC) 491 { 492 import core.memory: GC; 493 GC.addRange(n, Node.sizeof); 494 } 495 n.items[0] = move(mutable(item)); 496 n.markUsed(0); 497 return n; 498 } 499 500 void deallocateNode(Node* n) 501 { 502 if (n.prev !is null) 503 n.prev.next = n.next; 504 if (n.next !is null) 505 n.next.prev = n.prev; 506 if (_front is n) 507 _front = n.next; 508 if (_back is n) 509 _back = n.prev; 510 511 static if (useGC) 512 { 513 import core.memory: GC; 514 GC.removeRange(n); 515 } 516 allocator.dispose(n); 517 } 518 519 static bool shouldMerge(const Node* first, const Node* second) 520 { 521 if (first is null || second is null) 522 return false; 523 version (LDC_64) 524 { 525 import ldc.intrinsics : llvm_ctpop; 526 527 immutable f = llvm_ctpop(first.registry); 528 immutable s = llvm_ctpop(second.registry); 529 } 530 else 531 { 532 import containers.internal.backwards : popcnt; 533 534 immutable f = popcnt(first.registry); 535 immutable s = popcnt(second.registry); 536 } 537 return f + s <= nodeCapacity; 538 } 539 540 void mergeNodes(Node* first, Node* second) 541 in 542 { 543 assert (first !is null, "Invalid merge"); 544 assert (second !is null, "Invalid merge"); 545 assert (second is first.next, "Invalid merge"); 546 } 547 do 548 { 549 size_t i; 550 ContainerStorageType!T[nodeCapacity] temp; 551 foreach (j; 0 .. nodeCapacity) 552 if (!first.isFree(j)) 553 temp[i++] = move(first.items[j]); 554 foreach (j; 0 .. nodeCapacity) 555 if (!second.isFree(j)) 556 temp[i++] = move(second.items[j]); 557 foreach (j; 0 .. i) 558 first.items[j] = move(temp[j]); 559 first.registry = 0; 560 foreach (k; 0 .. i) 561 first.markUsed(k); 562 assert (first.registry <= fullBitPattern, "Overflow"); 563 deallocateNode(second); 564 } 565 566 static struct Node 567 { 568 size_t nextAvailableIndex() const nothrow pure @safe @nogc 569 { 570 static if (BookkeepingType.sizeof < uint.sizeof) 571 immutable uint notReg = ~(cast(uint) registry); 572 else 573 immutable auto notReg = ~registry; 574 version (LDC_64) 575 { 576 import ldc.intrinsics : llvm_cttz; 577 return llvm_cttz(notReg, true); 578 } 579 else 580 { 581 import containers.internal.backwards : bsf; 582 return bsf(notReg); 583 } 584 } 585 586 void markUsed(size_t index) nothrow pure @safe @nogc 587 { 588 registry |= (BookkeepingType(1) << index); 589 } 590 591 void markUnused(size_t index) nothrow pure @safe @nogc 592 { 593 registry &= ~(BookkeepingType(1) << index); 594 static if (shouldNullSlot!T) 595 items[index] = null; 596 } 597 598 bool empty() const nothrow pure @safe @nogc 599 { 600 return registry == 0; 601 } 602 603 bool isFree(size_t index) const nothrow pure @safe @nogc 604 { 605 return (registry & (BookkeepingType(1) << index)) == 0; 606 } 607 608 debug(EMSI_CONTAINERS) invariant() 609 { 610 import std.string : format; 611 assert (registry <= fullBitPattern, format("%016b %016b", registry, fullBitPattern)); 612 assert (prev !is &this, "Infinite loop"); 613 assert (next !is &this, "Infinite loop"); 614 } 615 616 BookkeepingType registry; 617 ContainerStorageType!T[nodeCapacity] items; 618 Node* prev; 619 Node* next; 620 } 621 } 622 623 version(emsi_containers_unittest) unittest 624 { 625 import std.algorithm : equal; 626 import std.range : iota; 627 import std.string : format; 628 UnrolledList!ubyte l; 629 static assert (l.Node.sizeof <= 64); 630 assert (l.empty); 631 l.insert(0); 632 assert (l.length == 1); 633 assert (!l.empty); 634 foreach (i; 1 .. 100) 635 l.insert(cast(ubyte) i); 636 assert (l.length == 100); 637 assert (equal(l[], iota(100))); 638 foreach (i; 0 .. 100) 639 assert (l.remove(cast(ubyte) i), format("%d", i)); 640 assert (l.length == 0, format("%d", l.length)); 641 assert (l.empty); 642 643 assert(*l.insert(1) == 1); 644 assert(*l.insert(2) == 2); 645 assert (l.remove(1)); 646 assert (!l.remove(1)); 647 assert (!l.empty); 648 649 UnrolledList!ubyte l2; 650 l2.insert(1); 651 l2.insert(2); 652 l2.insert(3); 653 assert (l2.front == 1); 654 l2.popFront(); 655 assert (l2.front == 2); 656 assert (equal(l2[], [2, 3])); 657 l2.popFront(); 658 assert (equal(l2[], [3])); 659 l2.popFront(); 660 assert (l2.empty, format("%d", l2.front)); 661 assert (equal(l2[], cast(int[]) [])); 662 UnrolledList!int l3; 663 foreach (i; 0 .. 200) 664 l3.insert(i); 665 foreach (i; 0 .. 200) 666 { 667 auto x = l3.moveFront(); 668 assert (x == i, format("%d %d", i, x)); 669 } 670 assert (l3.empty); 671 foreach (i; 0 .. 200) 672 l3.insert(i); 673 assert (l3.length == 200); 674 foreach (i; 0 .. 200) 675 { 676 assert (l3.length == 200 - i); 677 auto x = l3.moveBack(); 678 assert (x == 200 - i - 1, format("%d %d", 200 - 1 - 1, x)); 679 } 680 assert (l3.empty); 681 } 682 683 version(emsi_containers_unittest) unittest 684 { 685 struct A { int a; int b; } 686 UnrolledList!(const(A)) objs; 687 objs.insert(A(10, 11)); 688 static assert (is (typeof(objs.front) == const)); 689 static assert (is (typeof(objs[].front) == const)); 690 } 691 692 version(emsi_containers_unittest) unittest 693 { 694 static class A 695 { 696 int a; 697 int b; 698 699 this(int a, int b) 700 { 701 this.a = a; 702 this.b = b; 703 } 704 } 705 706 UnrolledList!(A) objs; 707 objs.insert(new A(10, 11)); 708 } 709 710 // Issue #52 711 version(emsi_containers_unittest) unittest 712 { 713 UnrolledList!int list; 714 list.insert(0); 715 list.insert(0); 716 list.insert(0); 717 list.insert(0); 718 list.insert(0); 719 720 foreach (ref it; list[]) 721 it = 1; 722 723 foreach (it; list[]) 724 assert(it == 1); 725 } 726 727 // Issue #53 728 version(emsi_containers_unittest) unittest 729 { 730 UnrolledList!int ints; 731 ints.insertBack(0); 732 ints.insertBack(0); 733 734 ints.front = 1; 735 ints.back = 11; 736 737 assert(ints.front == 1); 738 assert(ints.back == 11); 739 } 740 741 // Issue #168 742 version(emsi_containers_unittest) unittest 743 { 744 import std.typecons : RefCounted; 745 alias E = RefCounted!int; 746 747 E e = E(12); 748 UnrolledList!E ints; 749 ints.insertBack(e); 750 ints.clear(); 751 // crucial: no assert failure 752 assert (e == 12); 753 } 754 755 // Issue #170 756 version(emsi_containers_unittest) unittest 757 { 758 static struct S { @disable this(this); } 759 UnrolledList!S list; 760 761 list.insert(S()); 762 list.remove(S()); 763 764 list.insert(S()); 765 S s; 766 list.remove(s); 767 }