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 containers.internal.node : shouldAddGCRange; 11 private import std.experimental.allocator.mallocator : Mallocator; 12 13 /** 14 * Unrolled Linked List. 15 * 16 * Nodes are (by default) sized to fit within a 64-byte cache line. The number 17 * of items stored per node can be read from the $(B nodeCapacity) field. 18 * See_also: $(LINK http://en.wikipedia.org/wiki/Unrolled_linked_list) 19 * Params: 20 * T = the element type 21 * Allocator = the allocator to use. Defaults to `Mallocator`. 22 * supportGC = true to ensure that the GC scans the nodes of the unrolled 23 * list, false if you are sure that no references to GC-managed memory 24 * will be stored in this container. 25 * cacheLineSize = Nodes will be sized to fit within this number of bytes. 26 */ 27 struct UnrolledList(T, Allocator = Mallocator, 28 bool supportGC = shouldAddGCRange!T, size_t cacheLineSize = 64) 29 { 30 this(this) @disable; 31 32 private import std.experimental.allocator.common : stateSize; 33 34 static if (stateSize!Allocator != 0) 35 { 36 /// No default construction if an allocator must be provided. 37 this() @disable; 38 39 /** 40 * Use the given `allocator` for allocations. 41 */ 42 this(Allocator allocator) 43 in 44 { 45 assert(allocator !is null); 46 } 47 body 48 { 49 this.allocator = allocator; 50 } 51 } 52 53 ~this() 54 { 55 clear(); 56 } 57 58 /** 59 * Removes all items from the list 60 */ 61 void clear() 62 { 63 Node* prev = null; 64 Node* cur = _front; 65 debug (EMSI_CONTAINERS) 66 { 67 ulong nodeCount = 0; 68 for (Node* c = _front; c !is null; c = c.next) 69 ++nodeCount; 70 } 71 while (cur !is null) 72 { 73 prev = cur; 74 cur = cur.next; 75 static if (!is(T == class)) 76 foreach (ref item; cur.items) 77 typeid(T).destroy(&item); 78 deallocateNode(prev); 79 } 80 _length = 0; 81 } 82 83 /** 84 * Inserts the given item into the end of the list. 85 */ 86 void insertBack(T item) 87 { 88 if (_back is null) 89 { 90 assert (_front is null); 91 _back = allocateNode(item); 92 _front = _back; 93 } 94 else 95 { 96 size_t index = _back.nextAvailableIndex(); 97 if (index >= nodeCapacity) 98 { 99 Node* n = allocateNode(item); 100 n.prev = _back; 101 _back.next = n; 102 _back = n; 103 index = 0; 104 } 105 else 106 { 107 _back.items[index] = item; 108 _back.markUsed(index); 109 } 110 } 111 _length++; 112 assert (_back.registry <= fullBits!nodeCapacity); 113 } 114 115 /** 116 * Inserts the given range into the end of the list 117 */ 118 void insertBack(R)(auto ref R range) 119 { 120 foreach (ref r; range) 121 insertBack(r); 122 } 123 124 /// ditto 125 alias put = insertBack; 126 /// ditto 127 alias insert = insertBack; 128 129 /** 130 * Inserts the given item in the frontmost available cell, which may put the 131 * item anywhere in the list as removal may leave gaps in list nodes. Use 132 * this only if the order of elements is not important. 133 */ 134 void insertAnywhere(T item) 135 { 136 Node* n = _front; 137 while (_front !is null) 138 { 139 size_t i = n.nextAvailableIndex(); 140 if (i >= nodeCapacity) 141 { 142 if (n.next is null) 143 break; 144 n = n.next; 145 continue; 146 } 147 n.items[i] = item; 148 n.markUsed(i); 149 _length++; 150 assert (n.registry <= fullBits!nodeCapacity); 151 return; 152 } 153 assert (n is _back); 154 n = allocateNode(item); 155 _back.next = n; 156 _back = n; 157 _length++; 158 assert (_back.registry <= fullBits!nodeCapacity); 159 } 160 161 /// Returns: the length of the list 162 size_t length() const nothrow pure @property @safe @nogc 163 { 164 return _length; 165 } 166 167 /// Returns: true if the list is empty 168 bool empty() const nothrow pure @property @safe @nogc 169 { 170 return _length == 0; 171 } 172 173 /** 174 * Removes the given item from the list. 175 * Returns: true if something was removed. 176 */ 177 bool remove(T item) 178 { 179 import core.bitop : popcnt; 180 if (_front is null) 181 return false; 182 bool retVal = false; 183 loop: for (Node* n = _front; n !is null; n = n.next) 184 { 185 foreach (i; 0 .. nodeCapacity) 186 { 187 if (n.items[i] == item) 188 { 189 n.markUnused(i); 190 --_length; 191 retVal = true; 192 if (n.registry == 0) 193 deallocateNode(n); 194 else if (shouldMerge(n, n.next)) 195 mergeNodes(n, n.next); 196 else if (shouldMerge(n.prev, n)) 197 mergeNodes(n.prev, n); 198 break loop; 199 } 200 } 201 } 202 return retVal; 203 } 204 205 /// Pops the front item off of the list 206 void popFront() 207 { 208 moveFront(); 209 assert (_front is null || _front.registry != 0); 210 } 211 212 /// Pops the front item off of the list and returns it 213 T moveFront() 214 in 215 { 216 assert (!empty()); 217 assert (_front.registry != 0); 218 } 219 body 220 { 221 import core.bitop : bsf, popcnt; 222 size_t index = bsf(_front.registry); 223 T r = _front.items[index]; 224 _front.markUnused(index); 225 _length--; 226 if (_front.registry == 0) 227 { 228 auto f = _front; 229 if (_front.next !is null) 230 _front.next.prev = null; 231 assert (_front.next !is _front); 232 _front = _front.next; 233 if (_front is null) 234 _back = null; 235 else 236 assert (_front.registry <= fullBits!nodeCapacity); 237 deallocateNode(f); 238 return r; 239 } 240 if (shouldMerge(_front, _front.next)) 241 mergeNodes(_front, _front.next); 242 return r; 243 } 244 245 debug (EMSI_CONTAINERS) invariant 246 { 247 import std..string: format; 248 assert (_front is null || _front.registry != 0, format("%x, %b", _front, _front.registry)); 249 assert (_front !is null || _back is null); 250 if (_front !is null) 251 { 252 const(Node)* c = _front; 253 while (c.next !is null) 254 c = c.next; 255 assert(c is _back, "_back pointer is wrong"); 256 } 257 } 258 259 /** 260 * Time complexity is O(1) 261 * Returns: the item at the front of the list 262 */ 263 inout(T) front() inout @property 264 in 265 { 266 assert (!empty); 267 assert (_front.registry != 0); 268 } 269 body 270 { 271 import core.bitop: bsf; 272 import std..string: format; 273 size_t index = bsf(_front.registry); 274 assert (index < nodeCapacity, format("%d", index)); 275 return _front.items[index]; 276 } 277 278 /** 279 * Time complexity is O(n) 280 * Returns: the item at the back of the list 281 */ 282 inout(T) back() inout @property 283 in 284 { 285 assert (!empty); 286 assert (!_back.empty); 287 } 288 body 289 { 290 size_t i = nodeCapacity - 1; 291 while (_back.isFree(i)) 292 i--; 293 return _back.items[i]; 294 } 295 296 /// Pops the back item off of the list. 297 void popBack() 298 { 299 moveBack(); 300 } 301 302 /// Removes an item from the back of the list and returns it. 303 T moveBack() 304 in 305 { 306 assert (!empty); 307 assert (!_back.empty); 308 } 309 body 310 { 311 import core.bitop : popcnt; 312 size_t i = nodeCapacity - 1; 313 while (_back.isFree(i)) 314 { 315 if (i == 0) 316 break; 317 else 318 i--; 319 } 320 assert (!_back.isFree(i)); 321 T item = _back.items[i]; 322 _back.markUnused(i); 323 _length--; 324 if (_back.registry == 0) 325 { 326 deallocateNode(_back); 327 return item; 328 } 329 else if (shouldMerge(_back.prev, _back)) 330 mergeNodes(_back.prev, _back); 331 return item; 332 } 333 334 /** 335 * Number of items stored per node. 336 */ 337 enum size_t nodeCapacity = fatNodeCapacity!(T.sizeof, 2, ushort, cacheLineSize); 338 339 /// Returns: a range over the list 340 auto range(this This)() const nothrow pure @nogc @trusted @property 341 { 342 return Range!(This)(_front); 343 } 344 345 /// ditto 346 alias opSlice = range; 347 348 static struct Range(ThisT) 349 { 350 @disable this(); 351 352 this(inout(Node)* current) 353 { 354 import core.bitop: bsf; 355 this.current = current; 356 if (current !is null) 357 { 358 index = bsf(current.registry); 359 assert (index < nodeCapacity); 360 } 361 } 362 363 ET front() const @property @trusted @nogc 364 { 365 return cast(T) current.items[index]; 366 } 367 368 void popFront() nothrow pure 369 { 370 index++; 371 while (true) 372 { 373 if (current is null) 374 return; 375 if (index >= nodeCapacity) 376 { 377 current = current.next; 378 index = 0; 379 } 380 else 381 { 382 if (current.isFree(index)) 383 index++; 384 else 385 return; 386 } 387 } 388 } 389 390 bool empty() const nothrow pure @property @safe @nogc 391 { 392 return current is null; 393 } 394 395 Range save() const nothrow pure @property @safe @nogc 396 { 397 return this; 398 } 399 400 private: 401 402 alias ET = ContainerElementType!(ThisT, T); 403 const(Node)* current; 404 size_t index; 405 } 406 407 private: 408 409 import std.experimental.allocator: make, dispose; 410 import containers.internal.node : fatNodeCapacity, shouldAddGCRange, 411 fullBits, shouldNullSlot; 412 import containers.internal.storage_type : ContainerStorageType; 413 import containers.internal.element_type : ContainerElementType; 414 private import containers.internal.mixins : AllocatorState; 415 416 enum bool useGC = supportGC && shouldAddGCRange!T; 417 418 Node* _back; 419 Node* _front; 420 size_t _length; 421 mixin AllocatorState!Allocator; 422 423 Node* allocateNode(T item) 424 { 425 Node* n = make!Node(allocator); 426 static if (useGC) 427 { 428 import core.memory: GC; 429 GC.addRange(n, Node.sizeof); 430 } 431 n.items[0] = item; 432 n.markUsed(0); 433 return n; 434 } 435 436 void deallocateNode(Node* n) 437 { 438 if (n.prev !is null) 439 n.prev.next = n.next; 440 if (n.next !is null) 441 n.next.prev = n.prev; 442 if (_front is n) 443 _front = n.next; 444 if (_back is n) 445 _back = n.prev; 446 447 allocator.dispose(n); 448 static if (useGC) 449 { 450 import core.memory: GC; 451 GC.removeRange(n); 452 } 453 } 454 455 static bool shouldMerge(const Node* first, const Node* second) 456 { 457 import core.bitop : popcnt; 458 459 if (first is null || second is null) 460 return false; 461 immutable f = popcnt(first.registry); 462 immutable s = popcnt(second.registry); 463 return f + s <= nodeCapacity; 464 } 465 466 void mergeNodes(Node* first, Node* second) 467 in 468 { 469 assert (first !is null); 470 assert (second !is null); 471 assert (second is first.next); 472 } 473 body 474 { 475 import core.bitop: bsf; 476 size_t i; 477 ContainerStorageType!T[nodeCapacity] temp; 478 foreach (j; 0 .. nodeCapacity) 479 if (!first.isFree(j)) 480 temp[i++] = first.items[j]; 481 foreach (j; 0 .. nodeCapacity) 482 if (!second.isFree(j)) 483 temp[i++] = second.items[j]; 484 first.items[0 .. i] = temp[0 .. i]; 485 first.registry = 0; 486 foreach (k; 0 .. i) 487 first.markUsed(k); 488 assert (first.registry <= fullBits!nodeCapacity); 489 deallocateNode(second); 490 } 491 492 static struct Node 493 { 494 size_t nextAvailableIndex() const nothrow pure @safe @nogc 495 { 496 import core.bitop: bsf; 497 return bsf(~registry); 498 } 499 500 void markUsed(size_t index) nothrow pure @safe @nogc 501 { 502 registry |= (1 << index); 503 } 504 505 void markUnused(size_t index) nothrow pure @safe @nogc 506 { 507 registry &= ~(1 << index); 508 static if (shouldNullSlot!T) 509 items[index] = null; 510 } 511 512 bool empty() const nothrow pure @safe @nogc 513 { 514 return registry == 0; 515 } 516 517 bool isFree(size_t index) const nothrow pure @safe @nogc 518 { 519 return (registry & (1 << index)) == 0; 520 } 521 522 debug(EMSI_CONTAINERS) invariant() 523 { 524 import std..string : format; 525 assert (registry <= fullBits!nodeCapacity, format("%016b %016b", registry, fullBits!nodeCapacity)); 526 assert (prev !is &this); 527 assert (next !is &this); 528 } 529 530 ushort registry; 531 ContainerStorageType!T[nodeCapacity] items; 532 Node* prev; 533 Node* next; 534 } 535 } 536 537 unittest 538 { 539 import std.algorithm : equal; 540 import std.range : iota; 541 import std..string : format; 542 UnrolledList!int l; 543 static assert (l.Node.sizeof <= 64); 544 assert (l.empty); 545 l.insert(0); 546 assert (l.length == 1); 547 assert (!l.empty); 548 foreach (i; 1 .. 100) 549 l.insert(i); 550 assert (l.length == 100); 551 assert (equal(l[], iota(100))); 552 foreach (i; 0 .. 100) 553 assert (l.remove(i), format("%d", i)); 554 assert (l.length == 0, format("%d", l.length)); 555 assert (l.empty); 556 UnrolledList!int l2; 557 l2.insert(1); 558 l2.insert(2); 559 l2.insert(3); 560 assert (l2.front == 1); 561 l2.popFront(); 562 assert (l2.front == 2); 563 assert (equal(l2[], [2, 3])); 564 l2.popFront(); 565 assert (equal(l2[], [3])); 566 l2.popFront(); 567 assert (l2.empty, format("%d", l2.front)); 568 assert (equal(l2[], cast(int[]) [])); 569 UnrolledList!int l3; 570 foreach (i; 0 .. 200) 571 l3.insert(i); 572 foreach (i; 0 .. 200) 573 { 574 auto x = l3.moveFront(); 575 assert (x == i, format("%d %d", i, x)); 576 } 577 assert (l3.empty); 578 foreach (i; 0 .. 200) 579 l3.insert(i); 580 assert (l3.length == 200); 581 foreach (i; 0 .. 200) 582 { 583 assert (l3.length == 200 - i); 584 auto x = l3.moveBack(); 585 assert (x == 200 - i - 1, format("%d %d", 200 - 1 - 1, x)); 586 } 587 assert (l3.empty); 588 } 589 590 unittest 591 { 592 struct A { int a; int b; } 593 UnrolledList!(const(A)) objs; 594 objs.insert(A(10, 11)); 595 static assert (is (typeof(objs.front) == const)); 596 static assert (is (typeof(objs[].front) == const)); 597 }