1 /** 2 Implements a range algorithm that caches element values turns InputRangess to 3 RandomAccessRanges. 4 */ 5 6 module alid.cached; 7 8 import alid.errornogc : NogcError; 9 import alid.circularblocks : CircularBlocks; 10 import core.memory : pageSize; 11 import std.range : ElementType; 12 13 /** 14 A _range algorithm that caches the elements of a _range by evaluating them 15 only once. 16 17 As a natural benefit of caching, the returned _range is a `ForwardRange` 18 even if the source _range is an `InputRange`. Although the returned _range 19 provides `opIndex`, it is a `RandomAccessRange` only if the source _range 20 `hasLength`. 21 22 The version that takes buffers may never need to allocate memory if the 23 number of elements never exceed the capacity of the underlying 24 `CircularBlocks`. (See `CircularBlocks`.) 25 26 Params: 27 28 range = the source _range to cache elements of 29 30 heapBlockCapacity = the minimum capacity to use for `CircularBlocks`, 31 which is used as storage for the _cached elements; 32 the default value attemps to use one page of memory 33 34 buffers = the _buffers to be used by the `CircularBlocks` member 35 36 Bugs: 37 38 Although `opIndex` is provided, `isRandomAccessRange` produces `false` 39 because this implementation is not a `BidirectionalRange` at this time. 40 41 Todo: 42 43 Add `opApplyReverse` support and `BidirectionalRange` functions 44 45 */ 46 auto cached(R)(R range, size_t heapBlockCapacity = pageSize / ElementType!R.sizeof) 47 { 48 // Makes a new ElementCache object that will be kept alive collectively with 49 // the slices that it produces, first of which is returned. 50 51 if (heapBlockCapacity == 0) 52 { 53 heapBlockCapacity = 100; 54 } 55 56 auto elements = CircularBlocks!(ElementType!R)(heapBlockCapacity); 57 auto theCacheObject = new ElementCache!R(range, elements, heapBlockCapacity); 58 59 // This first slice starts with offset 0 60 enum elementOffset = 0; 61 return theCacheObject.makeSlice(elementOffset); 62 } 63 64 /// Ditto 65 auto cached(R, size_t N, size_t M)(R range, ref ubyte[N][M] buffers) 66 { 67 // Makes a new ElementCache object that will be kept alive collectively with 68 // the slices that it produces, first of which is returned. 69 70 auto elements = CircularBlocks!(ElementType!R)(buffers); 71 enum heapBlockCapacity = N; 72 auto theCacheObject = new ElementCache!R(range, elements, heapBlockCapacity); 73 74 enum elementOffset = 0; 75 return theCacheObject.makeSlice(elementOffset); 76 } 77 78 /// 79 unittest 80 { 81 // In the following typical usage, even though `slide(2)` would visit most 82 // elements more than once, `cached` guarantees that the lambda function 83 // executed by `map` will be executed only once per range element. 84 85 auto r = iota(4) 86 .map!(i => i * i) // [0, 1, 4, 9] 87 .cached 88 .slide(2) 89 .array; 90 91 // There are 3 sliding windows of width 2 over 4 elements 92 r.length.shouldBe(3); 93 assert(r[0].equal([0, 1])); 94 assert(r[1].equal([1, 4])); 95 assert(r[2].equal([4, 9])); 96 } 97 98 /// 99 unittest 100 { 101 // Random access over an InputRange 102 103 import std.algorithm : splitter; 104 import std.range : popFrontN; 105 106 auto lines = "monday,tuesday,wednesday,thursday"; 107 108 auto s = lines.splitter(','); 109 auto c = s.cached; 110 111 // The source range does not provide random access: 112 static assert(!__traits(compiles, s[0])); 113 114 // The cached range does: 115 assert(c[2] == "wednesday"); 116 assert(c[1] == "tuesday"); 117 118 c.popFrontN(3); 119 120 // Now index 0 is another element 121 assert(c[0] == "thursday"); 122 } 123 124 /// 125 unittest 126 { 127 // This version uses an existing buffer. It may never need to allocate 128 // additional heap memory. 129 130 ubyte[1024][2] buffers; 131 auto r = iota(4) 132 .map!(i => i * i) 133 .cached(buffers) // Will use the provided buffers 134 .slide(2) 135 .array; 136 137 r.length.shouldBe(3); 138 } 139 140 private mixin NogcError!"cached"; 141 142 /* 143 This is the implementation of the actual cache, elements of which will be 144 shared by potentially multiple CachedRange ranges. 145 */ 146 private struct ElementCache(R) 147 { 148 import std.array : empty; 149 import std.range : hasLength, ElementType; 150 import std.traits : isUnsigned; 151 152 // Useful aliases and values 153 alias ET = ElementType!R; 154 alias invalidSliceOffset = CachedRange!ElementCache.invalidSliceOffset; 155 enum rangeHasLength = hasLength!R; 156 157 R range; // The source range 158 CircularBlocks!ET elements; // The cached elements 159 size_t[] sliceOffsets; // The beginning indexes of slices into 'elements' 160 161 size_t liveSliceCount; // The number of slices that are still being used 162 size_t dropLeadingAttempts; // The number of times we considered but did 163 // not go with dropping leading elements 164 165 size_t minElementsToDrop; // The number of elements we consider large 166 // enough to consider for dropping 167 168 Stats stats_; 169 170 @disable this(this); 171 @disable this(ref const(typeof(this))); 172 173 this(R range, ref CircularBlocks!ET elements, size_t heapBlockCapacity) 174 { 175 import std.algorithm : move; 176 177 this.range = range; 178 this.elements = move(elements); 179 180 // Make it the same as heap capacity because it is likely that the 181 // underlying circular buffer will keep the elements alive below this 182 // figure anyway. 183 this.minElementsToDrop = heapBlockCapacity; 184 } 185 186 Stats stats() const @nogc nothrow pure @safe scope 187 { 188 Stats result = stats_; 189 // Unlike other statistics figures, this value is not kept up-do-date by 190 // us 191 result.heapAllocations = elements.heapAllocations; 192 return result; 193 } 194 195 // Whether the parameter is valid as a slice id 196 bool isValidId_(in size_t id) const @nogc nothrow pure @safe scope 197 { 198 return id < sliceOffsets.length; 199 } 200 201 // mixin string for throwing an Error 202 enum idError_ = `cachedError("Invalid id", id)`; 203 204 // Whether the specified slice is empty 205 bool emptyOf(in size_t id) pure scope 206 in (isValidId_(id), mixin (idError_)) 207 { 208 if (!range.empty) { 209 expandAsNeeded(id, 1); 210 } 211 212 return (sliceOffsets[id] == elements.length) && range.empty; 213 } 214 215 // The front element of the specified slice 216 auto frontOf(in size_t id) scope 217 in (isValidId_(id), mixin (idError_)) 218 { 219 expandAsNeeded(id, 1); 220 return elements[sliceOffsets[id]]; 221 } 222 223 // Pop an element from the specified slice 224 void popFrontOf(in size_t id) @nogc nothrow pure @safe scope 225 in (isValidId_(id), mixin (idError_)) 226 { 227 // Trivially increment the offset. 228 ++sliceOffsets[id]; 229 230 if (sliceOffsets[id] >= minElementsToDrop) { 231 /* 232 This slice has popped enough elements for us to consider dropping 233 leading elements. But we don't want to rush to it yet because even 234 determining whether there are unused elements or not incur some 235 cost. 236 237 We will apply the heuristic rule of ensuring this condition has 238 been seen at least as many times as there are live slices. (Even a 239 single slice is sufficient to hold on to the leading elements.) 240 */ 241 ++dropLeadingAttempts; 242 243 if (dropLeadingAttempts >= liveSliceCount) { 244 // We waited long enough 245 dropLeadingAttempts = 0; 246 const minIndex = unreferencedLeadingElements(); 247 248 if (minIndex) { 249 // There are leading elements that are not being used 250 // anymore 251 dropLeadingElements(minIndex); 252 253 // TODO: Commenting out for now because we can't be sure of 254 // usage patterns to decide to remove blocks from the 255 // underlying storage. (See a related 'version' in one 256 // of the unittest blocks.) 257 version (RUN_COMPACTION) 258 { 259 // Since we've dropped elements, let's also consider 260 // dropping unused blocks from the underlying circular 261 // buffer. 262 const occupancy = elements.heapBlockOccupancy; 263 264 if (occupancy.occupied && 265 (occupancy.total > occupancy.occupied * 4)) { 266 ++stats_.compactionRuns; 267 const removedBlocks = elements.compact(); 268 stats_.removedBlocks += removedBlocks; 269 } 270 } // version (none) 271 } 272 } 273 } 274 } 275 276 // The specified element (index) of the specified slice (id). 277 auto getElementOf(in size_t id, in size_t index) scope 278 in (isValidId_(id), mixin (idError_)) 279 { 280 expandAsNeeded(id, index + 1); 281 return elements[sliceOffsets[id] + index]; 282 } 283 284 // Make and return a new range object that has the same beginning index as 285 // the specified slice 286 auto saveOf(in size_t id) nothrow pure scope 287 in (isValidId_(id), mixin (idError_)) 288 { 289 return makeSlice(sliceOffsets[id]); 290 } 291 292 static if (rangeHasLength) 293 { 294 // The length of the specified slice 295 auto lengthOf(in size_t id) @nogc nothrow pure @safe scope 296 in (isValidId_(id), mixin (idError_)) 297 { 298 return range.length + elements.length - sliceOffsets[id]; 299 } 300 } 301 302 // Create and return a RefCounted!CachedRange that initially references all 303 // currently cached elements beginning from the specified offset (element 304 // index) 305 auto makeSlice(in size_t offset) 306 { 307 import std.algorithm : find, move; 308 import std.typecons : refCounted; 309 310 // Find an unused spot in the slice offsets array. 311 auto found = sliceOffsets.find(invalidSliceOffset); 312 const id = sliceOffsets.length - found.length; 313 314 if (id == sliceOffsets.length) 315 { 316 // There is no unused spot; add one. 317 sliceOffsets ~= offset; 318 } 319 else 320 { 321 sliceOffsets[id] = offset; 322 } 323 324 ++liveSliceCount; 325 326 // Return a reference counted object so that its destructor will 327 // "unregister" it by removing it from the list of slices. 328 auto slice = CachedRange!ElementCache(&this, id); 329 return refCounted(move(slice)); 330 } 331 332 // Remove the specified slice; it does not own any element anymore 333 void removeSlice(in size_t id) 334 in (isValidId_(id), mixin (idError_)) 335 { 336 sliceOffsets[id] = invalidSliceOffset; 337 --liveSliceCount; 338 } 339 340 // Determine the number of leading elements that are not being referenced 341 // (used) by any slice 342 size_t unreferencedLeadingElements() @nogc nothrow pure @safe scope 343 { 344 auto minOffset = size_t.max; 345 346 foreach (offset; sliceOffsets) 347 { 348 // There is no reason to continue because 0 is the absolute minimum 349 // for unsigned types. 350 if (offset == minOffset.min) 351 { 352 return offset; 353 } 354 355 if (offset < minOffset) 356 { 357 minOffset = offset; 358 } 359 } 360 361 return minOffset; 362 } 363 364 // Drop specified number of leading elements from the cache 365 void dropLeadingElements(size_t n) @nogc nothrow pure @safe scope 366 { 367 import std.algorithm : each, filter; 368 369 // Drop the unreferenced elements 370 elements.removeFrontN(n); 371 372 // Adjust the starting indexes of all slices accordingly. 373 sliceOffsets 374 .filter!((ref i) => i != invalidSliceOffset) 375 .each!((ref i) => i -= n); 376 377 ++stats_.leadingDropRuns; 378 stats_.droppedElements += n; 379 } 380 381 // Ensure that the specified slice has elements as needed 382 void expandAsNeeded(in size_t id, in size_t needed) scope 383 in (isValidId_(id), mixin (idError_)) 384 in (sliceOffsets[id] <= elements.length, 385 cachedError("Invalid element index", id, sliceOffsets[id], elements.length)) 386 { 387 // expandCache may change both elements and sliceOffsets. 388 while ((elements.length - sliceOffsets[id]) < needed) 389 { 390 expandCache(); 391 } 392 } 393 394 // Expand the element cache by adding one more element from the source range 395 void expandCache() scope 396 in (!range.empty) 397 { 398 import std.range : front, popFront; 399 400 // Transfer one element from the source range to the cache 401 elements.emplaceBack(range.front); 402 range.popFront(); 403 } 404 } 405 406 /** 407 Statistics about the operation of `ElementCache` as well as the underlying 408 `CircularBlocks` that it uses for element storage. 409 */ 410 static struct Stats 411 { 412 /// Number of blocks allocated from the heap by the underlying 413 /// `CircularBlocks` storage 414 size_t heapAllocations; 415 416 /// Number of times the algorithm for dropping leading elements is executed 417 size_t leadingDropRuns; 418 419 /// Number of leading elements dropped 420 size_t droppedElements; 421 422 /// Number of times `CircularBlocks.compact` is executed 423 size_t compactionRuns; 424 425 /// Number of blocks removed due to compaction 426 size_t removedBlocks; 427 428 void toString(scope void delegate(in char[]) sink) const scope 429 { 430 import std.format : formattedWrite; 431 432 sink.formattedWrite!"heap blocks allocated : %s\n"(heapAllocations); 433 sink.formattedWrite!"leading-element-drop executions : %s\n"(leadingDropRuns); 434 sink.formattedWrite!"total elements dropped : %s\n"(droppedElements); 435 sink.formattedWrite!"block compactions executions : %s\n"(compactionRuns); 436 sink.formattedWrite!"blocks removed due to compaction: %s\n"(removedBlocks); 437 } 438 } 439 440 version (unittest) 441 { 442 import alid.test : shouldBe; 443 import std.algorithm : equal, filter, map; 444 import std.array : array; 445 import std.range : iota, slide; 446 } 447 448 /** 449 Represents a `ForwardRange` (and a `RandomAccessRange` when possible) over 450 the cached elements of a source range. Provides the range algorithm 451 `length()` only if the source range does so. 452 453 Params: 454 455 EC = the template instance of the `ElementCache` template that this 456 range provides access to the elements of. The users are expected to 457 call one of the `cached` functions, which sets this parameter 458 automatically. 459 */ 460 // All pre-condition checks are handled by dispatched ElementCache member functions. 461 struct CachedRange(EC) 462 { 463 private: 464 465 EC * elementCache; 466 size_t id = size_t.max; 467 468 enum invalidSliceOffset = size_t.max; 469 470 // We can't allow copying objects of this type because they are intended to 471 // be reference counted. The users must call save() instead. 472 @disable this(this); 473 474 // Takes the ElementCache object that this is a slice of, and the assigned 475 // id of this slice 476 this(EC * elementCache, in size_t id) 477 { 478 this.elementCache = elementCache; 479 this.id = id; 480 } 481 482 public: 483 484 /** 485 Unregisters this slices from the actual `ElementCache` storage. 486 */ 487 ~this() @nogc nothrow pure @safe scope 488 { 489 // Prevent running on an .init state, which move() leaves behind 490 if (elementCache !is null) 491 { 492 elementCache.removeSlice(id); 493 } 494 } 495 496 /** 497 Return statistics about the operation of `ElementCache` as well as the 498 underlying `CircularBlocks` that it uses for element storage 499 */ 500 Stats stats() const @nogc nothrow pure @safe scope 501 { 502 return elementCache.stats; 503 } 504 505 /** 506 Support for `foreach` loops 507 */ 508 // This is needed to support foreach iteration because although 509 // RefCounted!CachedRange implicitly converts to CachedRange, which would be 510 // a range, the result gets copied to the foreach loop. Unfortunately, this 511 // type does not allow copying so we have to support foreach iteration 512 // explicitly here. 513 int opApply(int delegate(ref EC.ET) func) scope 514 { 515 while(!empty) 516 { 517 auto f = front; 518 int result = func(f); 519 if (result) 520 { 521 return result; 522 } 523 popFront(); 524 } 525 526 return 0; 527 } 528 529 /// Ditto 530 int opApply(int delegate(ref size_t, ref EC.ET) func) scope 531 { 532 size_t counter; 533 while(!empty) 534 { 535 auto f = front; 536 int result = func(counter, f); 537 if (result) 538 { 539 return result; 540 } 541 popFront(); 542 ++counter; 543 } 544 545 return 0; 546 } 547 548 // InputRange functions 549 550 /// Whether the range is empty 551 auto empty() pure scope 552 { 553 return elementCache.emptyOf(id); 554 } 555 556 /// The front element of the range 557 auto front() scope 558 { 559 return elementCache.frontOf(id); 560 } 561 562 /// Remove the front element from the range 563 void popFront() @nogc nothrow pure @safe scope 564 { 565 elementCache.popFrontOf(id); 566 } 567 568 // ForwardRange function 569 570 /// Make and return a new `ForwardRange` object that is the equivalent of 571 /// this range object 572 auto save() nothrow pure scope 573 { 574 return elementCache.saveOf(id); 575 } 576 577 // RandomAccessRange function 578 579 /** 580 Return a reference to an element 581 582 The algorithmic complexity of this function is amortized *O(1)* because 583 it might need to cache elements between the currently available last 584 element and the element about to be accessed with the specified index. 585 586 Params: 587 588 index = the _index of the element to return 589 */ 590 auto opIndex(in size_t index) scope 591 { 592 return elementCache.getElementOf(id, index); 593 } 594 595 // An optional length 596 597 static if (EC.rangeHasLength) 598 { 599 /** 600 Number of elements of this range 601 602 This function is available only if the source range provides it. 603 */ 604 auto length() @nogc nothrow pure @safe scope 605 { 606 return elementCache.lengthOf(id); 607 } 608 } 609 } 610 611 unittest 612 { 613 // Should compile and work with D arrays 614 615 assert([1, 2].cached.equal([1, 2])); 616 } 617 618 unittest 619 { 620 // 0 as heapBlockCapacity should work 621 622 auto r = iota(10).cached(0); 623 r[5].shouldBe(5); 624 assert(r.equal(iota(10))); 625 } 626 627 unittest 628 { 629 // InputRange, ForwardRange, save, and .length() tests 630 631 import std.range : take; 632 633 auto r = iota(1, 11).cached; 634 assert(!r.empty); 635 r.front.shouldBe(1); 636 r.length.shouldBe(10); 637 638 r.popFront(); 639 assert(!r.empty); 640 r.front.shouldBe(2); 641 r.length.shouldBe(9); 642 643 assert(r.take(4).equal(iota(2, 6))); 644 assert(!r.empty); 645 r.front.shouldBe(6); 646 r.length.shouldBe(5); 647 648 auto r2 = r.save(); 649 assert(r2.equal(iota(6, 11))); 650 assert(r2.empty); 651 r2.length.shouldBe(0); 652 653 assert(r.equal(iota(6, 11))); 654 assert(r.empty); 655 r.length.shouldBe(0); 656 } 657 658 unittest 659 { 660 // Iteration tests 661 662 import std.traits : isIterable; 663 664 enum begin = 1; 665 auto r = iota(begin, 11).cached; 666 static assert (isIterable!(typeof(r))); 667 668 auto r2 = r.save(); 669 670 foreach (e; r) 671 { 672 if (e == 3) 673 { 674 break; 675 } 676 } 677 r.front.shouldBe(3); 678 679 // Fully consume for code coverage 680 foreach (e; r) 681 { 682 } 683 assert(r.empty); 684 685 foreach (i, e; r2) 686 { 687 e.shouldBe(i + begin); 688 689 if (e == 7) 690 { 691 break; 692 } 693 } 694 r2.front.shouldBe(7); 695 696 // Fully consume for code coverage 697 foreach (i, e; r2) 698 { 699 } 700 assert(r2.empty); 701 } 702 703 unittest 704 { 705 // RandomAccessRange tests 706 707 import std.algorithm : each; 708 709 auto r = iota(0, 7).cached; 710 iota(5, 0, -1).each!(i => r[i].shouldBe(i)); 711 } 712 713 unittest 714 { 715 // hasLength tests 716 717 import std.range : hasLength; 718 719 { 720 auto r = iota(10).cached; 721 static assert (hasLength!(typeof(r))); 722 } 723 { 724 auto r = iota(10).filter!(i => i % 2).cached; 725 static assert (!hasLength!(typeof(r))); 726 } 727 } 728 729 unittest 730 { 731 // Although it is discouraged to have side-effects in a map lambda, let's test 732 // with such a map to prove that the lambda is executed only once per element. 733 734 import std.algorithm : find; 735 736 enum n = 42; 737 enum nonExistingValue = n + 1; 738 size_t counter = 0; 739 740 // Ensure all operations will be evaluated by searching for nonExistingValue 741 // in all elements of all sliding window ranges. 742 auto found = iota(n) 743 .map!((i) 744 { 745 ++counter; 746 return i; 747 }) 748 .cached 749 .slide(3) 750 .find!(window => !window.find(nonExistingValue).empty); 751 752 // Validate the test 753 assert(found.empty); 754 755 counter.shouldBe(n); 756 } 757 758 unittest 759 { 760 // Test that map expressions used by zip are cached as filter works on them. 761 762 import std.array : array; 763 import std.range : zip; 764 765 enum n = 10; 766 size_t count; 767 768 auto r = zip(iota(0, n) 769 .map!(_ => ++count)) 770 .cached 771 .filter!(t => t[0] == t[0]) // Normally, multiple execution 772 .array; 773 774 count.shouldBe(n); 775 } 776 777 unittest 778 { 779 // Out-of-bounds access should throw 780 781 import std.exception : assertThrown; 782 783 assertThrown!Error( 784 { 785 auto r = iota(5).cached; 786 int result = 0; 787 788 // More access than elements available 789 foreach (i; 0 .. 10) 790 { 791 result += r.front; 792 r.popFront(); 793 } 794 }()); 795 } 796 797 unittest 798 { 799 // The following is the code that exposed a difference between Rust's 800 // tuple_window and D's slide. tuple_window uses once() in its 801 // implementation so the generator lambda is executed only once per 802 // element. D's slide calls map's front multiple times. .cached is supposed 803 // to prevent that. 804 805 import std.array : array; 806 807 static struct Data 808 { 809 int * ptr; 810 size_t capacity; 811 } 812 813 enum n = 1_000; 814 815 int[] v; 816 817 auto r = iota(n) 818 .map!((i) 819 { 820 v ~= i; 821 return Data(v.ptr, v.capacity); 822 }) 823 .cached // The lambda should be executed once per object 824 .slide(2) // [0,1], [1,2], [2,3], etc. 825 .filter!((t) 826 { 827 const prev = t.front.ptr; 828 t.popFront(); 829 const curr = t.front.ptr; 830 return prev != curr; 831 }) 832 .map!(t => t.front.capacity) 833 .array; 834 835 v.length.shouldBe(n); 836 } 837 838 unittest 839 { 840 // A single slice should be sufficient to keep all elements alive 841 842 import std.algorithm : each; 843 import std.conv : to; 844 845 // Picking a large number of elements along with very small capacity to 846 // cause at least one consideration of dropping the front elements 847 enum n = 10_000; 848 enum heapBlockCapacity = 5; 849 850 auto r = iota(n).cached(1000); 851 852 { 853 void consume(A)(ref A a) 854 { 855 while(!a.empty) 856 { 857 a.popFront(); 858 } 859 } 860 861 // Create saved states of the range 862 auto saveds = iota(4).map!(_ => r.save()).array; 863 864 // This operation will ensure populating the element cache 865 consume(r); 866 867 // No leading element should have been dropped 868 assert(saveds[0].length == n); 869 870 // Consume all saved states but one 871 foreach (ref s; saveds[1..$]) 872 { 873 consume(s); 874 } 875 876 // Still, no leading element should have been dropped 877 assert(saveds[0].length == n); 878 879 // Finally, there should be dropping of leading elements 880 consume(saveds[0]); 881 } 882 883 // We expect non-zero figures 884 const stats = r.stats; 885 assert(stats.heapAllocations); 886 assert(stats.leadingDropRuns); 887 assert(stats.droppedElements); 888 version (RUN_COMPACTION) 889 { 890 assert(stats.compactionRuns); 891 assert(stats.removedBlocks); 892 } else { 893 assert(stats.compactionRuns == 0); 894 assert(stats.removedBlocks == 0); 895 } 896 897 stats.to!string; 898 }