The OpenD Programming Language

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*) &current.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 }