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