1 /** 2 Defines `Vec`, `reallocBuffer` and memory functions. 3 4 Copyright: Guillaume Piolat 2015-2024. 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 Authors: Guillaume Piolat 7 */ 8 module dplug.core.vec; 9 10 import std.traits: hasElaborateDestructor; 11 12 import core.stdc.stdlib: malloc, free, realloc; 13 import core.stdc.string: memcpy, memmove; 14 15 import core.exception; 16 import inteli.xmmintrin; 17 18 19 // This module deals with aligned memory. 20 // You'll also find here a non-copyable std::vector equivalent `Vec`. 21 22 /// Allocates an aligned memory chunk. 23 /// Functionally equivalent to Visual C++ _aligned_malloc. 24 /// Do not mix allocations with different alignment. 25 /// 26 /// Important: `alignedMalloc(0)` does not necessarily 27 /// return `null`, and its result 28 /// _has_ to be freed with `alignedFree`. 29 /// 30 /// Important: Don't call that often in a real-time thread 31 /// without amortization (keep a capacity). 32 /// If you need to allocate from `nextBuffer`, do 33 /// prefer the use of `Vec` which doesn't shrink, 34 /// and will reuse the allocation. 35 void* alignedMalloc(size_t size, size_t alignment) nothrow @nogc 36 { 37 assert(alignment != 0); 38 39 // Short-cut and use the C allocator to avoid overhead if no alignment 40 if (alignment == 1) 41 { 42 // C99: 43 // Implementation-defined behavior 44 // Whether the calloc, malloc, and realloc functions return a null pointer 45 // or a pointer to an allocated object when the size requested is zero. 46 // In any case, we'll have to free() it. 47 return malloc(size); 48 } 49 50 size_t request = requestedSize(size, alignment); 51 void* raw = malloc(request); 52 53 if (request > 0 && raw == null) // malloc(0) can validly return anything 54 onOutOfMemoryError(); 55 56 return storeRawPointerPlusSizeAndReturnAligned(raw, size, alignment); 57 } 58 59 /// Frees aligned memory allocated by alignedMalloc or alignedRealloc. 60 /// Functionally equivalent to Visual C++ _aligned_free. 61 /// Do not mix allocations with different alignment. 62 void alignedFree(void* aligned, size_t alignment) nothrow @nogc 63 { 64 // Short-cut and use the C allocator to avoid overhead if no alignment 65 if (alignment == 1) 66 return free(aligned); 67 68 // support for free(NULL) 69 if (aligned is null) 70 return; 71 72 assert(alignment != 0); 73 assert(isPointerAligned(aligned, alignment)); 74 75 void** rawLocation = cast(void**)(cast(char*)aligned - size_t.sizeof); 76 free(*rawLocation); 77 } 78 79 /// Reallocates an aligned memory chunk allocated by `alignedMalloc` or `alignedRealloc`. 80 /// Functionally equivalent to Visual C++ `_aligned_realloc`. 81 /// Do not mix allocations with different alignment. 82 /// 83 /// The "discard" version doesn't preserve data. 84 /// 85 /// Important: `alignedRealloc(p, 0)` does not necessarily return `null`, and its result 86 /// _has_ to be freed with `alignedFree`. 87 /// 88 /// Important: Don't call that often in a real-time thread 89 /// without amortization (keep a capacity). 90 /// If you need to allocate from `nextBuffer`, do 91 /// prefer the use of `Vec` which doesn't shrink, 92 /// and will reuse the allocation. 93 void* alignedRealloc(void* aligned, size_t size, size_t alignment) nothrow @nogc 94 { 95 return alignedReallocImpl!true(aligned, size, alignment); 96 } 97 ///ditto 98 void* alignedReallocDiscard(void* aligned, size_t size, size_t alignment) nothrow @nogc 99 { 100 return alignedReallocImpl!false(aligned, size, alignment); 101 } 102 103 104 /// Returns: `true` if the pointer is suitably aligned. 105 bool isPointerAligned(void* p, size_t alignment) pure nothrow @nogc 106 { 107 assert(alignment != 0); 108 return ( cast(size_t)p & (alignment - 1) ) == 0; 109 } 110 unittest 111 { 112 ubyte b; 113 align(16) ubyte[5] c; 114 assert(isPointerAligned(&b, 1)); 115 assert(!isPointerAligned(&c[1], 2)); 116 assert(isPointerAligned(&c[4], 4)); 117 } 118 119 /// Does memory slices a[0..a_size] and b[0..b_size] have an 120 /// overlapping byte? 121 bool isMemoryOverlapping(const(void)* a, ptrdiff_t a_size, 122 const(void)* b, ptrdiff_t b_size) 123 pure @trusted 124 { 125 assert(a_size >= 0 && b_size >= 0); 126 127 if (a is null || b is null) 128 return false; 129 130 if (a_size == 0 || b_size == 0) 131 return false; 132 133 ubyte* lA = cast(ubyte*)a; 134 ubyte* hA = lA + a_size; 135 ubyte* lB = cast(ubyte*)b; 136 ubyte* hB = lB + b_size; 137 138 // There is overlapping, if lA is inside lB..hB, 139 // or lB is inside lA..hA 140 141 if (lA >= lB && lA < hB) 142 return true; 143 144 if (lB >= lA && lB < hA) 145 return true; 146 147 return false; 148 } 149 bool isMemoryOverlapping(const(void)[] a, const(void)[] b) pure @trusted 150 { 151 return isMemoryOverlapping(a.ptr, a.length, b.ptr, b.length); 152 } 153 unittest 154 { 155 ubyte[100] a; 156 assert(!isMemoryOverlapping(null, a)); 157 assert(!isMemoryOverlapping(a, null)); 158 assert(!isMemoryOverlapping(a[1..1], a[0..10])); 159 assert(!isMemoryOverlapping(a[1..10], a[10..100])); 160 assert(!isMemoryOverlapping(a[30..100], a[0..30])); 161 assert(isMemoryOverlapping(a[1..50], a[49..100])); 162 assert(isMemoryOverlapping(a[49..100], a[1..50])); 163 assert(isMemoryOverlapping(a[40..45], a[30..55])); 164 assert(isMemoryOverlapping(a[30..55], a[40..45])); 165 } 166 167 private nothrow @nogc 168 { 169 void* alignedReallocImpl(bool PreserveDataIfResized)(void* aligned, size_t size, size_t alignment) 170 { 171 // Short-cut and use the C allocator to avoid overhead if no alignment 172 if (alignment == 1) 173 { 174 // C99: 175 // Implementation-defined behavior 176 // Whether the calloc, malloc, and realloc functions return a null pointer 177 // or a pointer to an allocated object when the size requested is zero. 178 // In any case, we'll have to `free()` it. 179 return realloc(aligned, size); 180 } 181 182 if (aligned is null) 183 return alignedMalloc(size, alignment); 184 185 assert(alignment != 0); 186 assert(isPointerAligned(aligned, alignment)); 187 188 size_t previousSize = *cast(size_t*)(cast(char*)aligned - size_t.sizeof * 2); 189 190 void* raw = *cast(void**)(cast(char*)aligned - size_t.sizeof); 191 size_t request = requestedSize(size, alignment); 192 size_t previousRequest = requestedSize(previousSize, alignment); 193 assert(previousRequest - request == previousSize - size); // same alignment 194 195 // Heuristic: if a requested size is within 50% to 100% of what is already allocated 196 // then exit with the same pointer 197 if ( (previousRequest < request * 4) && (request <= previousRequest) ) 198 return aligned; 199 200 void* newRaw = malloc(request); 201 static if( __VERSION__ > 2067 ) // onOutOfMemoryError wasn't nothrow before July 2014 202 { 203 if (request > 0 && newRaw == null) // realloc(0) can validly return anything 204 onOutOfMemoryError(); 205 } 206 207 void* newAligned = storeRawPointerPlusSizeAndReturnAligned(newRaw, size, alignment); 208 209 static if (PreserveDataIfResized) 210 { 211 size_t minSize = size < previousSize ? size : previousSize; 212 memcpy(newAligned, aligned, minSize); // memcpy OK 213 } 214 215 // Free previous data 216 alignedFree(aligned, alignment); 217 assert(isPointerAligned(newAligned, alignment)); 218 return newAligned; 219 } 220 221 /// Returns: next pointer aligned with alignment bytes. 222 void* nextAlignedPointer(void* start, size_t alignment) pure 223 { 224 import dplug.core.math : nextMultipleOf; 225 return cast(void*)nextMultipleOf(cast(size_t)(start), alignment); 226 } 227 228 // Returns number of bytes to actually allocate when asking 229 // for a particular alignement 230 size_t requestedSize(size_t askedSize, size_t alignment) pure 231 { 232 enum size_t pointerSize = size_t.sizeof; 233 return askedSize + alignment - 1 + pointerSize * 2; 234 } 235 236 // Store pointer given my malloc, and size in bytes initially requested (alignedRealloc needs it) 237 void* storeRawPointerPlusSizeAndReturnAligned(void* raw, size_t size, size_t alignment) 238 { 239 enum size_t pointerSize = size_t.sizeof; 240 char* start = cast(char*)raw + pointerSize * 2; 241 void* aligned = nextAlignedPointer(start, alignment); 242 void** rawLocation = cast(void**)(cast(char*)aligned - pointerSize); 243 *rawLocation = raw; 244 size_t* sizeLocation = cast(size_t*)(cast(char*)aligned - 2 * pointerSize); 245 *sizeLocation = size; 246 assert( isPointerAligned(aligned, alignment) ); 247 return aligned; 248 } 249 } 250 251 unittest 252 { 253 { 254 void* p = alignedMalloc(23, 16); 255 assert(p !is null); 256 assert(((cast(size_t)p) & 0xf) == 0); 257 258 alignedFree(p, 16); 259 } 260 261 void* nullAlloc = alignedMalloc(0, 16); 262 assert(nullAlloc != null); 263 nullAlloc = alignedRealloc(nullAlloc, 0, 16); 264 assert(nullAlloc != null); 265 alignedFree(nullAlloc, 16); 266 267 { 268 int alignment = 16; 269 int* p = null; 270 271 // check if growing keep values in place 272 foreach(int i; 0..100) 273 { 274 p = cast(int*) alignedRealloc(p, (i + 1) * int.sizeof, alignment); 275 p[i] = i; 276 } 277 278 foreach(int i; 0..100) 279 assert(p[i] == i); 280 281 p = cast(int*) alignedRealloc(p, 0, alignment); 282 assert(p !is null); 283 284 alignedFree(p, alignment); 285 } 286 287 // Verify that same size alloc preserve pointer. 288 { 289 void* p = null; 290 p = alignedRealloc(p, 254, 16); 291 void* p2 = alignedRealloc(p, 254, 16); 292 assert(p == p2); 293 294 // Test shrink heuristic 295 void* p3 = alignedRealloc(p, 128, 16); 296 assert(p == p3); 297 alignedFree(p3, 16); 298 } 299 } 300 301 302 303 /// Used throughout dplug:dsp to avoid reliance on GC. 304 /// Important: Size 0 is special-case to free the slice. 305 /// This works a bit like alignedRealloc except with slices as input. 306 /// You MUST use consistent alignement thoughout the lifetime of this buffer. 307 /// 308 /// Params: 309 /// buffer = Existing allocated buffer. Can be null. 310 /// Input slice length is not considered. 311 /// length = Desired slice length. 312 /// alignment = Alignement if the slice has allocation requirements, 1 else. 313 /// Must match for deallocation. 314 /// 315 /// Example: 316 /// --- 317 /// import std.stdio; 318 /// 319 /// struct MyDSP 320 /// { 321 /// nothrow @nogc: 322 /// 323 /// void initialize(int maxFrames) 324 /// { 325 /// // mybuf points to maxFrames frames 326 /// mybuf.reallocBuffer(maxFrames); 327 /// } 328 /// 329 /// ~this() 330 /// { 331 /// // If you don't free the buffer, it will leak. 332 /// mybuf.reallocBuffer(0); 333 /// } 334 /// 335 /// private: 336 /// float[] mybuf; 337 /// } 338 /// --- 339 /// Important: Don't call that often in a real-time thread 340 /// without amortization (keep a capacity). 341 /// If you need to allocate from `nextBuffer`, do 342 /// prefer the use of `Vec` which doesn't shrink, 343 /// and will reuse the allocation. 344 /// This, instead, will shrink the allocation 345 /// which makes it more expensive. 346 void reallocBuffer(T)(ref T[] buffer, size_t length, int alignment = 1) nothrow @nogc 347 { 348 static if (is(T == struct) && hasElaborateDestructor!T) 349 { 350 static assert(false); // struct with destructors not supported 351 } 352 353 /// Size 0 is special-case to free the slice. 354 if (length == 0) 355 { 356 alignedFree(buffer.ptr, alignment); 357 buffer = null; 358 return; 359 } 360 361 T* pointer = cast(T*) alignedRealloc(buffer.ptr, T.sizeof * length, alignment); 362 if (pointer is null) 363 buffer = null; // alignment 1 can still return null 364 else 365 buffer = pointer[0..length]; 366 } 367 unittest 368 { 369 int[] arr; 370 arr.reallocBuffer(15); 371 assert(arr.length == 15); 372 arr.reallocBuffer(0); 373 assert(arr.length == 0); 374 } 375 376 377 /// Returns: A newly created `Vec`. 378 Vec!T makeVec(T)(size_t initialSize = 0) nothrow @nogc 379 { 380 return Vec!T(initialSize); 381 } 382 383 deprecated("Vec!T must have alignment 1. This constructor will be removed in Dplug v16") 384 Vec!T makeVec(T)(size_t initialSize = 0, int alignment) nothrow @nogc 385 { 386 return Vec!T(initialSize, alignment); 387 } 388 389 /// Kind of a std::vector replacement. 390 /// Grow-only array, points to a (optionally aligned) memory location. 391 /// This can also work as an output range. 392 /// `Vec` is designed to work even when uninitialized, without `makeVec`. 393 /// Warning: it is pretty barebones, doesn't respect T.init or call destructors. 394 /// When used in a GC program, GC roots won't be registered. 395 struct Vec(T) 396 { 397 nothrow: 398 @nogc: 399 400 static if (is(T == struct) && hasElaborateDestructor!T) 401 { 402 static assert(false, "struct with destructors cannot go in `Vec!T`. You are welcome to use the `numem` package instead or do otherwise."); 403 } 404 405 public 406 { 407 /// Creates an aligned buffer with given initial size. 408 this(size_t initialSize) @safe 409 { 410 _size = 0; 411 _allocated = 0; 412 _data = null; 413 _alignment = 1; 414 resizeExactly(initialSize); 415 } 416 417 deprecated("Vec!T must have alignment 1. This constructor will be removed in Dplug v16") 418 this(size_t initialSize, int alignment) @safe 419 { 420 assert(alignment != 0); 421 _size = 0; 422 _allocated = 0; 423 _data = null; 424 _alignment = alignment; 425 resizeExactly(initialSize); 426 } 427 428 ~this() @trusted 429 { 430 if (_data !is null) 431 { 432 alignedFree(_data, _alignment); 433 _data = null; 434 _allocated = 0; 435 } 436 } 437 438 @disable this(this); 439 440 /// Returns: Length of buffer in elements. 441 size_t length() pure const @safe 442 { 443 return _size; 444 } 445 ///ditto 446 alias size = length; 447 448 /// Returns: Length of buffer in elements, as signed. 449 ptrdiff_t slength() pure const @safe 450 { 451 return _size; 452 } 453 ///ditto 454 alias ssize = length; 455 456 /// Returns: `true` if length is zero. 457 bool isEmpty() pure const @safe 458 { 459 return _size == 0; 460 } 461 462 /// Returns: Allocated size of the underlying array. 463 size_t capacity() pure const @safe 464 { 465 return _allocated; 466 } 467 468 /// Returns: Length of buffer in elements. 469 alias opDollar = length; 470 471 /// Resizes a buffer to hold $(D askedSize) elements. 472 void resize(size_t askedSize) @trusted 473 { 474 resizeExactly(askedSize); 475 } 476 477 /// Pop last element 478 T popBack() @trusted 479 { 480 assert(_size > 0); 481 _size = _size - 1; 482 return _data[_size]; 483 } 484 485 /// Append an element to this buffer. 486 void pushBack(T x) @trusted 487 { 488 size_t i = _size; 489 resizeGrow(_size + 1); 490 _data[i] = x; 491 } 492 493 // DMD 2.088 deprecates the old D1-operators 494 static if (__VERSION__ >= 2088) 495 { 496 ///ditto 497 void opOpAssign(string op)(T x) @safe if (op == "~") 498 { 499 pushBack(x); 500 } 501 } 502 else 503 { 504 ///ditto 505 void opCatAssign(T x) @safe 506 { 507 pushBack(x); 508 } 509 } 510 511 // Output range support 512 alias put = pushBack; 513 514 /// Finds an item, returns -1 if not found 515 int indexOf(T x) @trusted 516 { 517 enum bool isStaticArray(T) = __traits(isStaticArray, T); 518 519 static if (isStaticArray!T) 520 { 521 // static array would be compared by identity as slice, which is not what we want. 522 foreach(int i; 0..cast(int)_size) 523 if (_data[i] == x) 524 return i; 525 } 526 else 527 { 528 // base types: identity is equality 529 // reference types: looking for identity 530 foreach(int i; 0..cast(int)_size) 531 if (_data[i] is x) 532 return i; 533 } 534 return -1; 535 } 536 537 /// Removes an item and replaces it by the last item. 538 /// Warning: this reorders the array. 539 void removeAndReplaceByLastElement(size_t index) @trusted 540 { 541 assert(index < _size); 542 _data[index] = _data[--_size]; 543 } 544 545 /// Removes an item and shift the rest of the array to front by 1. 546 /// Warning: O(N) complexity. 547 void removeAndShiftRestOfArray(size_t index) @trusted 548 { 549 assert(index < _size); 550 for (; index + 1 < _size; ++index) 551 _data[index] = _data[index+1]; 552 --_size; 553 } 554 555 /// Removes a range of items and shift the rest of the array to front. 556 /// Warning: O(N) complexity. 557 void removeAndShiftRestOfArray(size_t first, size_t last) @trusted 558 { 559 assert(first <= last && first <= _size && (last <= _size)); 560 size_t remain = _size - last; 561 for (size_t n = 0; n < remain; ++n) 562 _data[first + n] = _data[last + n]; 563 _size -= (last - first); 564 } 565 566 /// Appends another buffer to this buffer. 567 void pushBack(ref Vec other) @trusted 568 { 569 size_t oldSize = _size; 570 resizeGrow(_size + other._size); 571 memmove(_data + oldSize, other._data, T.sizeof * other._size); 572 } 573 574 /// Appends a slice to this buffer. 575 /// `slice` should not belong to the same buffer _data. 576 void pushBack(T[] slice) @trusted 577 { 578 size_t oldSize = _size; 579 size_t newSize = _size + slice.length; 580 resizeGrow(newSize); 581 for (size_t n = 0; n < slice.length; ++n) 582 _data[oldSize + n] = slice[n]; 583 } 584 585 /// Returns: Raw pointer to data. 586 @property inout(T)* ptr() inout @system 587 { 588 return _data; 589 } 590 591 /// Returns: n-th element. 592 ref inout(T) opIndex(size_t i) pure inout @trusted 593 { 594 return _data[i]; 595 } 596 597 T opIndexAssign(T x, size_t i) pure @trusted 598 { 599 return _data[i] = x; 600 } 601 602 /// Sets size to zero, but keeps allocated buffers. 603 void clearContents() pure @safe 604 { 605 _size = 0; 606 } 607 608 /// Returns: Whole content of the array in one slice. 609 inout(T)[] opSlice() inout @safe 610 { 611 return opSlice(0, length()); 612 } 613 614 /// Returns: A slice of the array. 615 inout(T)[] opSlice(size_t i1, size_t i2) inout @trusted 616 { 617 return _data[i1 .. i2]; 618 } 619 620 /// Fills the buffer with the same value. 621 void fill(T x) @trusted 622 { 623 _data[0.._size] = x; 624 } 625 626 /// Move. Give up owner ship of the data. 627 T[] releaseData() @system 628 { 629 T[] data = _data[0.._size]; 630 assert(_alignment == 1); // else would need to be freed with alignedFree. 631 this._data = null; 632 this._size = 0; 633 this._allocated = 0; 634 this._alignment = 0; 635 return data; 636 } 637 } 638 639 private 640 { 641 size_t _size = 0; 642 T* _data = null; 643 size_t _allocated = 0; 644 size_t _alignment = 1; // for an unaligned Vec, you probably are not interested in alignment 645 646 /// Used internally to grow in response to a pushBack operation. 647 /// Different heuristic, since we know that the resize is likely to be repeated for an 648 /// increasing size later. 649 void resizeGrow(size_t askedSize) @trusted 650 { 651 if (_allocated < askedSize) 652 { 653 654 version(all) 655 { 656 size_t newCap = computeNewCapacity(askedSize, _size); 657 setCapacity(newCap); 658 } 659 else 660 { 661 setCapacity(2 * askedSize); 662 } 663 } 664 _size = askedSize; 665 } 666 667 // Resizes the `Vector` to hold exactly `askedSize` elements. 668 // Still if the allocated capacity is larger, do nothing. 669 void resizeExactly(size_t askedSize) @trusted 670 { 671 if (_allocated < askedSize) 672 { 673 setCapacity(askedSize); 674 } 675 _size = askedSize; 676 } 677 678 // Internal use, realloc internal buffer, copy existing items. 679 // Doesn't initialize the new ones. 680 void setCapacity(size_t cap) 681 { 682 size_t numBytes = cap * T.sizeof; 683 _data = cast(T*)(alignedRealloc(_data, numBytes, _alignment)); 684 _allocated = cap; 685 } 686 687 // Compute new capacity, while growing. 688 size_t computeNewCapacity(size_t newLength, size_t oldLength) 689 { 690 // Optimal value (Windows malloc) not far from there. 691 enum size_t PAGESIZE = 4096; 692 693 size_t newLengthBytes = newLength * T.sizeof; 694 if (newLengthBytes > PAGESIZE) 695 { 696 // Larger arrays need a smaller growth factor to avoid wasting too much bytes. 697 // This was found when tracing curve, not too far from golden ratio for some reason. 698 return newLength + newLength / 2 + newLength / 8; 699 } 700 else 701 { 702 // For smaller arrays being pushBack, can bring welcome speed by minimizing realloc. 703 return newLength * 3; 704 } 705 } 706 } 707 } 708 709 unittest 710 { 711 import std.range.primitives; 712 static assert(isOutputRange!(Vec!ubyte, ubyte)); 713 714 715 import std.random; 716 int NBUF = 200; 717 718 Xorshift32 rng; 719 rng.seed(0xBAADC0DE); 720 721 struct box2i { int a, b, c, d; } 722 Vec!box2i[] boxes; 723 boxes.length = NBUF; 724 725 foreach(i; 0..NBUF) 726 { 727 boxes[i] = makeVec!box2i(); 728 } 729 730 foreach(j; 0..200) 731 { 732 foreach(i; 0..NBUF) 733 { 734 int previousSize = cast(int)(boxes[i].length); 735 void* previousPtr = boxes[i].ptr; 736 foreach(int k; 0..cast(int)(boxes[i].length)) 737 boxes[i][k] = box2i(k, k, k, k); 738 739 int newSize = uniform(0, 100, rng); 740 boxes[i].resize(newSize); 741 742 int minSize = previousSize; 743 if (minSize > boxes[i].length) 744 minSize = cast(int)(boxes[i].length); 745 746 void* newPtr = boxes[i].ptr; 747 foreach(int k; 0..minSize) 748 { 749 box2i item = boxes[i][k]; 750 box2i shouldBe = box2i(k, k, k, k); 751 assert(item == shouldBe); 752 } 753 754 int sum = 0; 755 foreach(k; 0..newSize) 756 { 757 box2i bb = boxes[i][k]; 758 sum += bb.a + bb.b + bb.c + bb.d; 759 } 760 } 761 } 762 763 foreach(i; 0..NBUF) 764 boxes[i].destroy(); 765 766 { 767 auto buf = makeVec!int; 768 enum N = 10; 769 buf.resize(N); 770 foreach(i ; 0..N) 771 buf[i] = i; 772 773 foreach(i ; 0..N) 774 assert(buf[i] == i); 775 776 auto buf2 = makeVec!int; 777 buf2.pushBack(11); 778 buf2.pushBack(14); 779 780 // test pushBack(slice) 781 buf.clearContents(); 782 buf.pushBack(buf2[]); 783 assert(buf[0] == 11); 784 assert(buf[1] == 14); 785 786 // test pushBack(slice) 787 buf2[1] = 8; 788 buf.clearContents(); 789 buf.pushBack(buf2); 790 assert(buf[0] == 11); 791 assert(buf[1] == 8); 792 } 793 } 794 795 // Vec should work without any initialization 796 unittest 797 { 798 Vec!string vec; 799 800 foreach(e; vec[]) 801 { 802 } 803 804 assert(vec.length == 0); 805 assert(vec.size == 0); 806 assert(vec.slength == 0); 807 assert(vec.ssize == 0); 808 vec.clearContents(); 809 vec.resize(0); 810 assert(vec == vec.init); 811 vec.fill("filler"); 812 assert(vec.ptr is null); 813 } 814 815 // Issue #312: vec.opIndex not returning ref which break struct assignment 816 unittest 817 { 818 static struct A 819 { 820 int x; 821 } 822 Vec!A vec = makeVec!A(); 823 A a; 824 vec.pushBack(a); 825 vec ~= a; 826 vec[0].x = 42; // vec[0] needs to return a ref 827 assert(vec[0].x == 42); 828 } 829 unittest // removeAndShiftRestOfArray was wrong 830 { 831 Vec!int v; 832 v.pushBack(14); 833 v.pushBack(27); 834 v.pushBack(38); 835 assert(v.length == 3); 836 v.removeAndShiftRestOfArray(1); 837 assert(v.length == 2); 838 assert(v[] == [14, 38]); 839 } 840 unittest // removeAndShiftRestOfArray with slice 841 { 842 Vec!int v; 843 v.pushBack([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 844 v.removeAndShiftRestOfArray(3, 5); 845 assert(v[] == [0, 1, 2, 5, 6, 7, 8, 9]); 846 v.removeAndShiftRestOfArray(0, 4); 847 assert(v[] == [6, 7, 8, 9]); 848 v.removeAndShiftRestOfArray(2, 4); 849 assert(v[] == [6, 7]); 850 v.removeAndShiftRestOfArray(2, 2); 851 v.removeAndShiftRestOfArray(0, 0); 852 v.removeAndShiftRestOfArray(1, 1); 853 assert(v[] == [6, 7]); 854 } 855 856 857 858 /// Allows to merge the allocation of several arrays, which saves allocation count and can speed up things thanks to locality. 859 /// 860 /// Example: see below unittest. 861 struct MergedAllocation 862 { 863 nothrow: 864 @nogc: 865 866 // In debug mode, add stomp detection to `MergedAllocation`. 867 // This takes additional complexity to coarse check for stomping nearby buffers. 868 debug 869 { 870 private enum mergedAllocStompWarning = true; 871 } 872 else 873 { 874 private enum mergedAllocStompWarning = false; 875 } 876 877 878 // This adds 32-byte of sentinels around each allocation, 879 // and check at the end of the program that they were unaffected. 880 static if (mergedAllocStompWarning) 881 { 882 // Number of bytes to write between areas to check for stomping. 883 enum SENTINEL_BYTES = 32; 884 } 885 886 enum maxExpectedAlignment = 32; 887 888 /// Start defining the area of allocations. 889 void start() 890 { 891 // Detect memory errors in former uses. 892 static if (mergedAllocStompWarning) 893 { 894 checkSentinelAreas(); 895 _allocateWasCalled = false; 896 } 897 898 _base = cast(ubyte*)(cast(size_t)0); 899 } 900 901 /// Allocate (or count) space needed for `numElems` elements of type `T` with given alignment. 902 /// This gets called twice for each array, see example for usage. 903 /// 904 /// This bumps the internal bump allocator. 905 /// Giving null to this chain and converting the result to size_t give the total needed size 906 /// for the merged allocation. 907 /// 908 /// Warning: 909 /// - If called after a `start()` call, the area returned are wrong and are only for 910 /// counting needed bytes. Don't use those. 911 /// 912 /// - If called after an `allocate()` call, the area returned are an actual merged 913 /// allocation (if the same calls are done). 914 /// 915 /// Warning: Prefer `allocArray` over `alloc` variant, since the extra length field WILL help 916 /// you catch memory errors before release. Else it is very common to miss buffer 917 /// overflows in samplerate changes. 918 void allocArray(T)(out T[] array, size_t numElems, size_t alignment = 1) 919 { 920 assert(alignment <= maxExpectedAlignment); 921 assert( (alignment != 0) && ((alignment & (alignment - 1)) == 0)); // power of two 922 923 size_t adr = cast(size_t) _base; 924 925 // 1. Align base address 926 size_t mask = ~(alignment - 1); 927 adr = (adr + alignment - 1) & mask; 928 929 // 2. Assign array and base. 930 array = (cast(T*)adr)[0..numElems]; 931 adr += T.sizeof * numElems; 932 _base = cast(ubyte*) adr; 933 934 static if (mergedAllocStompWarning) 935 { 936 if (_allocateWasCalled && _allocation !is null) 937 { 938 // Each allocated area followed with SENTINEL_BYTES bytes of value 0xCC 939 _base[0..SENTINEL_BYTES] = 0xCC; 940 registerSentinel(_base); 941 } 942 _base += SENTINEL_BYTES; 943 } 944 } 945 946 ///ditto 947 void alloc(T)(out T* array, size_t numElems, size_t alignment = 1) 948 { 949 T[] arr; 950 allocArray(arr, numElems, alignment); 951 array = arr.ptr; 952 } 953 954 /// Allocate actual storage for the merged allocation. From there, you need to define exactly the same area with `alloc` and `allocArray`. 955 /// This time they will get a proper value. 956 void allocate() 957 { 958 static if (mergedAllocStompWarning) _allocateWasCalled = true; 959 960 size_t sizeNeeded = cast(size_t)_base; // since it was fed 0 at start. 961 962 if (sizeNeeded == 0) 963 { 964 // If no bytes are requested, it means no buffer were requested, or only with zero size. 965 // We will return a null pointer in that case, since accessing them would be illegal anyway. 966 _allocation = null; 967 } 968 else 969 { 970 // the merged allocation needs to have the largest expected alignment, else the size could depend on the hazards 971 // of the allocation. With maximum alignment, padding is the same so long as areas have smaller or equal alignment requirements. 972 _allocation = cast(ubyte*) _mm_realloc(_allocation, sizeNeeded, maxExpectedAlignment); 973 } 974 975 // So that the next layout call points to the right area. 976 _base = _allocation; 977 } 978 979 ~this() 980 { 981 static if (mergedAllocStompWarning) 982 { 983 checkSentinelAreas(); 984 } 985 986 if (_allocation != null) 987 { 988 _mm_free(_allocation); 989 _allocation = null; 990 } 991 } 992 993 private: 994 995 // Location of the allocation. 996 ubyte* _allocation = null; 997 998 /// 999 ubyte* _base = null; 1000 1001 static if (mergedAllocStompWarning) 1002 { 1003 bool _allocateWasCalled = false; 1004 1005 Vec!(ubyte*) _sentinels; // start of sentinel area (SENTINAL_BYTES long) 1006 1007 void registerSentinel(ubyte* start) 1008 { 1009 _sentinels.pushBack(start); 1010 } 1011 1012 bool hasMemoryError() // true if sentinel bytes stomped 1013 { 1014 assert(_allocateWasCalled && _allocation !is null); 1015 1016 foreach(ubyte* s; _sentinels[]) 1017 { 1018 for (int n = 0; n < 32; ++n) 1019 { 1020 if (s[n] != 0xCC) 1021 return true; 1022 } 1023 } 1024 return false; 1025 } 1026 1027 // Check existing sentinels, and unregister them 1028 void checkSentinelAreas() 1029 { 1030 if (!_allocateWasCalled) 1031 return; // still haven't done an allocation, nothing to check. 1032 1033 if (_allocation is null) 1034 return; // nothing to check 1035 1036 // If you fail here, there is a memory error in your access patterns. 1037 // Sentinel bytes of value 0xCC were overwritten in a `MergedAllocation`. 1038 // You can use slices with `allocArray` instead of `alloc` to find the faulty 1039 // access. This check doesn't catch everything! 1040 assert(! hasMemoryError()); 1041 1042 _sentinels.clearContents(); 1043 } 1044 } 1045 } 1046 1047 1048 // Here is how you should use MergedAllocation. 1049 unittest 1050 { 1051 static struct MyDSPStruct 1052 { 1053 public: 1054 nothrow: 1055 @nogc: 1056 void initialize(int maxFrames) 1057 { 1058 _mergedAlloc.start(); 1059 layout(_mergedAlloc, maxFrames); // you need such a layout function to be called twice. 1060 _mergedAlloc.allocate(); 1061 layout(_mergedAlloc, maxFrames); // the first time arrays area allocated in the `null` area, the second time in 1062 // actually allocated memory (since we now have the needed length). 1063 } 1064 1065 void layout(ref MergedAllocation ma, int maxFrames) 1066 { 1067 // allocate `maxFrames` elems, and return a slice in `_intermediateBuf`. 1068 ma.allocArray(_intermediateBuf, maxFrames); 1069 1070 // allocate `maxFrames` elems, aligned to 16-byte boundaries. Return a pointer to that in `_coeffs`. 1071 ma.alloc(_coeffs, maxFrames, 16); 1072 } 1073 1074 private: 1075 float[] _intermediateBuf; 1076 double* _coeffs; 1077 MergedAllocation _mergedAlloc; 1078 } 1079 1080 MyDSPStruct s; 1081 s.initialize(14); 1082 s._coeffs[0..14] = 1.0f; 1083 s._intermediateBuf[0..14] = 1.0f; 1084 s.initialize(17); 1085 s._coeffs[0..17] = 1.0f; 1086 s._intermediateBuf[0..17] = 1.0f; 1087 } 1088 1089 // Should be valid to allocate nothing with a MergedAllocation. 1090 unittest 1091 { 1092 MergedAllocation ma; 1093 ma.start(); 1094 ma.allocate(); 1095 assert(ma._allocation == null); 1096 } 1097 1098 // test stomping detection 1099 unittest 1100 { 1101 MergedAllocation ma; 1102 ma.start(); 1103 ubyte[] arr, arr2; 1104 ma.allocArray(arr, 17); 1105 ma.allocArray(arr2, 24); 1106 assert(ma._allocation == null); 1107 1108 ma.allocate(); 1109 ma.allocArray(arr, 17); 1110 ma.allocArray(arr2, 24); 1111 assert(ma._allocation != null); 1112 assert(arr.length == 17); 1113 assert(arr2.length == 24); 1114 1115 // Test memory error detection with a simple case. 1116 static if (MergedAllocation.mergedAllocStompWarning) 1117 { 1118 assert(!ma.hasMemoryError()); 1119 1120 // Create a memory error 1121 arr.ptr[18] = 2; 1122 assert(ma.hasMemoryError()); 1123 arr.ptr[18] = 0xCC; // undo the error to avoid stopping the unittests 1124 } 1125 }