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