| // Written in the D programming language. | |
| /** | |
| Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/bitmapped_block.d) | |
| */ | |
| module std.experimental.allocator.building_blocks.bitmapped_block; | |
| import std.experimental.allocator.building_blocks.null_allocator; | |
| import std.experimental.allocator.common; | |
| import std.typecons : Flag, Yes, No; | |
| // Common implementation for shared and non-shared versions of the BitmappedBlock | |
| private mixin template BitmappedBlockImpl(bool isShared, bool multiBlock) | |
| { | |
| import std.conv : text; | |
| import std.traits : hasMember; | |
| import std.typecons : Ternary; | |
| import std.typecons : tuple, Tuple; | |
| static if (isShared && multiBlock) | |
| import core.internal.spinlock : SpinLock; | |
| static assert(theBlockSize > 0 && theAlignment.isGoodStaticAlignment); | |
| static assert(theBlockSize == chooseAtRuntime || | |
| theBlockSize % theAlignment == 0, "Block size must be a multiple of the alignment"); | |
| static if (theBlockSize != chooseAtRuntime) | |
| { | |
| alias blockSize = theBlockSize; | |
| } | |
| else | |
| { | |
| // It is the caller's responsibilty to synchronize this with | |
| // allocate/deallocate in shared environments | |
| @property uint blockSize() { return _blockSize; } | |
| @property void blockSize(uint s) | |
| { | |
| static if (multiBlock) | |
| { | |
| assert((cast(BitVector) _control).length == 0 && s % alignment == 0); | |
| } | |
| else | |
| { | |
| assert(_control.length == 0 && s % alignment == 0); | |
| } | |
| _blockSize = s; | |
| } | |
| private uint _blockSize; | |
| } | |
| static if (is(ParentAllocator == NullAllocator)) | |
| { | |
| private enum parentAlignment = platformAlignment; | |
| } | |
| else | |
| { | |
| private alias parentAlignment = ParentAllocator.alignment; | |
| static assert(parentAlignment >= ulong.alignof); | |
| } | |
| alias alignment = theAlignment; | |
| static if (stateSize!ParentAllocator) | |
| { | |
| ParentAllocator parent; | |
| } | |
| else | |
| { | |
| alias parent = ParentAllocator.instance; | |
| } | |
| private size_t _blocks; | |
| private void[] _payload; | |
| private size_t _startIdx; | |
| // For multiblock, '_control' is a BitVector, otherwise just a regular ulong[] | |
| static if (multiBlock) | |
| { | |
| // Keeps track of first block which has never been used in an allocation. | |
| // All blocks which are located right to the '_freshBit', should have never been | |
| // allocated | |
| private ulong _freshBit; | |
| private BitVector _control; | |
| } | |
| else | |
| { | |
| private ulong[] _control; | |
| } | |
| static if (multiBlock && isShared) | |
| { | |
| SpinLock lock = SpinLock(SpinLock.Contention.brief); | |
| } | |
| pure nothrow @safe @nogc | |
| private size_t totalAllocation(size_t capacity) | |
| { | |
| auto blocks = capacity.divideRoundUp(blockSize); | |
| auto leadingUlongs = blocks.divideRoundUp(64); | |
| import std.algorithm.comparison : min; | |
| immutable initialAlignment = min(parentAlignment, | |
| 1U << min(31U, trailingZeros(leadingUlongs * 8))); | |
| auto maxSlack = alignment <= initialAlignment | |
| ? 0 | |
| : alignment - initialAlignment; | |
| return leadingUlongs * 8 + maxSlack + blockSize * blocks; | |
| } | |
| this(ubyte[] data) | |
| { | |
| immutable a = data.ptr.effectiveAlignment; | |
| assert(a >= size_t.alignof || !data.ptr, | |
| "Data must be aligned properly"); | |
| immutable ulong totalBits = data.length * 8; | |
| immutable ulong bitsPerBlock = blockSize * 8 + 1; | |
| _blocks = totalBits / bitsPerBlock; | |
| // Reality is a bit more complicated, iterate until a good number of | |
| // blocks found. | |
| size_t localBlocks; | |
| for (localBlocks = _blocks; localBlocks; --localBlocks) | |
| { | |
| immutable controlWords = localBlocks.divideRoundUp(64); | |
| auto payload = data[controlWords * 8 .. $].roundStartToMultipleOf( | |
| alignment); | |
| if (payload.length < localBlocks * blockSize) | |
| { | |
| // Overestimated | |
| continue; | |
| } | |
| // Need the casts for shared versions | |
| static if (multiBlock) | |
| { | |
| _control = cast(typeof(_control)) BitVector((cast(ulong*) data.ptr)[0 .. controlWords]); | |
| (cast(BitVector) _control)[] = 0; | |
| } | |
| else | |
| { | |
| _control = (cast(typeof(_control.ptr)) data.ptr)[0 .. controlWords]; | |
| _control[] = 0; | |
| } | |
| _payload = cast(typeof(_payload)) payload; | |
| break; | |
| } | |
| _blocks = cast(typeof(_blocks)) localBlocks; | |
| } | |
| static if (chooseAtRuntime == theBlockSize) | |
| this(ubyte[] data, uint blockSize) | |
| { | |
| this._blockSize = blockSize; | |
| this(data); | |
| } | |
| static if (!is(ParentAllocator == NullAllocator) && !stateSize!ParentAllocator) | |
| this(size_t capacity) | |
| { | |
| size_t toAllocate = totalAllocation(capacity); | |
| auto data = cast(ubyte[])(parent.allocate(toAllocate)); | |
| this(data); | |
| assert(_blocks * blockSize >= capacity); | |
| } | |
| static if (!is(ParentAllocator == NullAllocator) && stateSize!ParentAllocator) | |
| this(ParentAllocator parent, size_t capacity) | |
| { | |
| this.parent = parent; | |
| size_t toAllocate = totalAllocation(capacity); | |
| auto data = cast(ubyte[])(parent.allocate(toAllocate)); | |
| this(data); | |
| } | |
| static if (!is(ParentAllocator == NullAllocator) && | |
| chooseAtRuntime == theBlockSize && | |
| !stateSize!ParentAllocator) | |
| this(size_t capacity, uint blockSize) | |
| { | |
| this._blockSize = blockSize; | |
| this(capacity); | |
| } | |
| static if (!is(ParentAllocator == NullAllocator) && | |
| chooseAtRuntime == theBlockSize && | |
| stateSize!ParentAllocator) | |
| this(ParentAllocator parent, size_t capacity, uint blockSize) | |
| { | |
| this._blockSize = blockSize; | |
| this(parent, capacity); | |
| } | |
| static if (!is(ParentAllocator == NullAllocator) | |
| && hasMember!(ParentAllocator, "deallocate")) | |
| ~this() | |
| { | |
| // multiblock bitmapped blocks use a BitVector | |
| static if (multiBlock) | |
| { | |
| void* start = cast(void*) _control.rep.ptr; | |
| } | |
| else | |
| { | |
| void* start = cast(void*) _control.ptr; | |
| } | |
| void* end = cast(void*) (_payload.ptr + _payload.length); | |
| parent.deallocate(start[0 .. end - start]); | |
| } | |
| pure nothrow @safe @nogc | |
| size_t goodAllocSize(size_t n) | |
| { | |
| return n.roundUpToMultipleOf(blockSize); | |
| } | |
| // Implementation of the 'multiBlock' BitmappedBlock | |
| // For the shared version, the methods are protected by a common lock | |
| static if (multiBlock) | |
| { | |
| /* | |
| Adjusts the memoized _startIdx to the leftmost control word that has at | |
| least one zero bit. Assumes all control words to the left of $(D | |
| _control[_startIdx]) are already occupied. | |
| */ | |
| private void adjustStartIdx() | |
| { | |
| while (_startIdx < _control.rep.length && _control.rep[_startIdx] == ulong.max) | |
| { | |
| static if (isShared) | |
| { | |
| // Shared demands atomic increment, however this is protected | |
| // by a lock. Regular increment is fine | |
| auto localStart = _startIdx + 1; | |
| _startIdx = localStart; | |
| } | |
| else | |
| { | |
| ++_startIdx; | |
| } | |
| } | |
| } | |
| /* | |
| Based on the latest allocated bit, 'newBit', it adjusts '_freshBit' | |
| */ | |
| pure nothrow @safe @nogc | |
| private void adjustFreshBit(const ulong newBit) | |
| { | |
| import std.algorithm.comparison : max; | |
| static if (isShared) | |
| { | |
| auto localFreshBit = max(newBit, _freshBit); | |
| _freshBit = localFreshBit; | |
| } | |
| else | |
| { | |
| _freshBit = max(newBit, _freshBit); | |
| } | |
| } | |
| /* | |
| Returns the blocks corresponding to the control bits starting at word index | |
| wordIdx and bit index msbIdx (MSB=0) for a total of howManyBlocks. | |
| */ | |
| @trusted | |
| private void[] blocksFor(this _)(size_t wordIdx, uint msbIdx, size_t howManyBlocks) | |
| { | |
| assert(msbIdx <= 63); | |
| const start = (wordIdx * 64 + msbIdx) * blockSize; | |
| const end = start + blockSize * howManyBlocks; | |
| if (start == end) return null; | |
| if (end <= _payload.length) return cast(void[]) _payload[start .. end]; | |
| // This could happen if we have more control bits than available memory. | |
| // That's possible because the control bits are rounded up to fit in | |
| // 64-bit words. | |
| return null; | |
| } | |
| static if (isShared) | |
| nothrow @safe @nogc | |
| void[] allocate(const size_t s) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| return allocateImpl(s); | |
| } | |
| static if (!isShared) | |
| pure nothrow @safe @nogc | |
| void[] allocate(const size_t s) | |
| { | |
| return allocateImpl(s); | |
| } | |
| // If shared, this is protected by a lock inside 'allocate' | |
| pure nothrow @trusted @nogc | |
| private void[] allocateImpl(const size_t s) | |
| { | |
| const blocks = s.divideRoundUp(blockSize); | |
| void[] result; | |
| Lswitch: | |
| switch (blocks) | |
| { | |
| case 1: | |
| // inline code here for speed | |
| // find the next available block | |
| foreach (i; _startIdx .. _control.rep.length) | |
| { | |
| const w = _control.rep[i]; | |
| if (w == ulong.max) continue; | |
| uint j = leadingOnes(w); | |
| assert(j < 64, "Invalid number of blocks"); | |
| assert((_control.rep[i] & ((1UL << 63) >> j)) == 0, "Corrupted bitmap"); | |
| static if (isShared) | |
| { | |
| // Need the cast because shared does not recognize the lock | |
| *(cast(ulong*) &_control._rep[i]) |= (1UL << 63) >> j; | |
| } | |
| else | |
| { | |
| _control.rep[i] |= (1UL << 63) >> j; | |
| } | |
| if (i == _startIdx) | |
| { | |
| adjustStartIdx(); | |
| } | |
| result = blocksFor(i, j, 1); | |
| break Lswitch; | |
| } | |
| goto case 0; // fall through | |
| case 0: | |
| return null; | |
| case 2: .. case 64: | |
| result = smallAlloc(cast(uint) blocks); | |
| break; | |
| default: | |
| result = hugeAlloc(blocks); | |
| break; | |
| } | |
| if (result) | |
| { | |
| adjustFreshBit((result.ptr - _payload.ptr) / blockSize + blocks); | |
| } | |
| return result.ptr ? result.ptr[0 .. s] : null; | |
| } | |
| @trusted void[] allocateFresh(const size_t s) | |
| { | |
| static if (isShared) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| } | |
| const blocks = s.divideRoundUp(blockSize); | |
| void[] result = blocksFor(cast(size_t) (_freshBit / 64), | |
| cast(uint) (_freshBit % 64), blocks); | |
| if (result) | |
| { | |
| (cast(BitVector) _control)[_freshBit .. _freshBit + blocks] = 1; | |
| static if (isShared) | |
| { | |
| ulong localFreshBit = _freshBit; | |
| localFreshBit += blocks; | |
| _freshBit = localFreshBit; | |
| } | |
| else | |
| { | |
| _freshBit += blocks; | |
| } | |
| } | |
| return result; | |
| } | |
| void[] alignedAllocate(size_t n, uint a) | |
| { | |
| static if (isShared) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| } | |
| return alignedAllocateImpl(n, a); | |
| } | |
| // If shared, this is protected by a lock inside 'alignedAllocate' | |
| private void[] alignedAllocateImpl(size_t n, uint a) | |
| { | |
| import std.math.traits : isPowerOf2; | |
| assert(a.isPowerOf2); | |
| if (a <= alignment) return allocate(n); | |
| // Overallocate to make sure we can get an aligned block | |
| auto b = allocateImpl((n + a - alignment).roundUpToMultipleOf(blockSize)); | |
| if (!b.ptr) return null; | |
| auto result = b.roundStartToMultipleOf(a); | |
| assert(result.length >= n); | |
| result = result.ptr[0 .. n]; // final result | |
| // Free any blocks that might be slack at the beginning | |
| auto slackHeadingBlocks = (result.ptr - b.ptr) / blockSize; | |
| if (slackHeadingBlocks) | |
| { | |
| deallocateImpl(b[0 .. slackHeadingBlocks * blockSize]); | |
| } | |
| // Free any blocks that might be slack at the end | |
| auto slackTrailingBlocks = ((b.ptr + b.length) | |
| - (result.ptr + result.length)) / blockSize; | |
| if (slackTrailingBlocks) | |
| { | |
| deallocateImpl(b[$ - slackTrailingBlocks * blockSize .. $]); | |
| } | |
| return result; | |
| } | |
| /* | |
| Tries to allocate "blocks" blocks at the exact position indicated by the | |
| position wordIdx/msbIdx (msbIdx counts from MSB, i.e. MSB has index 0). If | |
| it succeeds, fills "result" with the result and returns tuple(size_t.max, | |
| 0). Otherwise, returns a tuple with the next position to search. | |
| */ | |
| private Tuple!(size_t, uint) allocateAt(size_t wordIdx, uint msbIdx, | |
| size_t blocks, ref void[] result) | |
| { | |
| assert(blocks > 0); | |
| assert(wordIdx < _control.rep.length); | |
| assert(msbIdx <= 63); | |
| void[] tmpResult; | |
| result = null; | |
| if (msbIdx + blocks <= 64) | |
| { | |
| // Allocation should fit this control word | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[wordIdx]; | |
| bool didSetBit = setBitsIfZero(localControl, | |
| cast(uint) (64 - msbIdx - blocks), 63 - msbIdx); | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| bool didSetBit = setBitsIfZero(_control.rep[wordIdx], | |
| cast(uint) (64 - msbIdx - blocks), 63 - msbIdx); | |
| } | |
| if (didSetBit) | |
| { | |
| tmpResult = blocksFor(wordIdx, msbIdx, blocks); | |
| if (!tmpResult) | |
| { | |
| static if (isShared) | |
| { | |
| localControl = _control.rep[wordIdx]; | |
| resetBits(localControl, | |
| cast(uint) (64 - msbIdx - blocks), 63 - msbIdx); | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| resetBits(_control.rep[wordIdx], | |
| cast(uint) (64 - msbIdx - blocks), 63 - msbIdx); | |
| } | |
| return tuple(size_t.max - 1, 0u); | |
| } | |
| result = tmpResult; | |
| tmpResult = null; | |
| return tuple(size_t.max, 0u); | |
| } | |
| // Can't allocate, make a suggestion | |
| return msbIdx + blocks == 64 | |
| ? tuple(wordIdx + 1, 0u) | |
| : tuple(wordIdx, cast(uint) (msbIdx + blocks)); | |
| } | |
| // Allocation spans two control words or more | |
| immutable mask = ulong.max >> msbIdx; | |
| if (_control.rep[wordIdx] & mask) | |
| { | |
| // We can't allocate the rest of this control word, | |
| // return a suggestion. | |
| return tuple(wordIdx + 1, 0u); | |
| } | |
| // We can allocate the rest of this control word, but we first need to | |
| // make sure we can allocate the tail. | |
| if (wordIdx + 1 == _control.rep.length) | |
| { | |
| // No more memory | |
| return tuple(_control.rep.length, 0u); | |
| } | |
| auto hint = allocateAt(wordIdx + 1, 0, blocks - 64 + msbIdx, result); | |
| if (hint[0] == size_t.max) | |
| { | |
| tmpResult = blocksFor(wordIdx, msbIdx, blocks); | |
| if (!tmpResult) | |
| { | |
| return tuple(size_t.max - 1, 0u); | |
| } | |
| static if (isShared) | |
| { | |
| // Dont want atomics, because this is protected by 'lock' | |
| ulong localControl = _control.rep[wordIdx]; | |
| localControl |= mask; | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| _control.rep[wordIdx] |= mask; | |
| } | |
| result = tmpResult; | |
| tmpResult = null; | |
| return tuple(size_t.max, 0u); | |
| } | |
| // Failed, return a suggestion that skips this whole run. | |
| return hint; | |
| } | |
| /* Allocates as many blocks as possible at the end of the blocks indicated | |
| by wordIdx. Returns the number of blocks allocated. */ | |
| private uint allocateAtTail(size_t wordIdx) | |
| { | |
| assert(wordIdx < _control.rep.length); | |
| const available = trailingZeros(_control.rep[wordIdx]); | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[wordIdx]; | |
| localControl |= ulong.max >> available; | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| _control.rep[wordIdx] |= ulong.max >> available; | |
| } | |
| return available; | |
| } | |
| pure nothrow @safe @nogc | |
| private void[] smallAlloc(uint blocks) return scope | |
| { | |
| assert(blocks >= 2 && blocks <= 64); | |
| void[] result; | |
| foreach (i; _startIdx .. _control.rep.length) | |
| { | |
| // Test within the current 64-bit word | |
| const v = _control.rep[i]; | |
| if (v == ulong.max) continue; | |
| auto j = findContigOnes(~v, blocks); | |
| if (j < 64) | |
| { | |
| // yay, found stuff | |
| result = blocksFor(i, j, blocks); | |
| if (result) | |
| { | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[i]; | |
| setBits(localControl, 64 - j - blocks, 63 - j); | |
| _control.rep[i] = localControl; | |
| } | |
| else | |
| { | |
| setBits(_control.rep[i], 64 - j - blocks, 63 - j); | |
| } | |
| } | |
| return result; | |
| } | |
| // Next, try allocations that cross a word | |
| auto available = trailingZeros(v); | |
| if (available == 0) continue; | |
| if (i + 1 >= _control.rep.length) break; | |
| assert(available < blocks); // otherwise we should have found it | |
| auto needed = blocks - available; | |
| assert(needed > 0 && needed < 64); | |
| result = blocksFor(i, 64 - available, blocks); | |
| if (result && allocateAtFront(i + 1, needed)) | |
| { | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[i]; | |
| localControl |= (1UL << available) - 1; | |
| _control.rep[i] = localControl; | |
| } | |
| else | |
| { | |
| _control.rep[i] |= (1UL << available) - 1; | |
| } | |
| return result; | |
| } | |
| } | |
| return null; | |
| } | |
| pure nothrow @trusted @nogc | |
| private void[] hugeAlloc(size_t blocks) return scope | |
| { | |
| assert(blocks > 64); | |
| if (_startIdx == _control._rep.length) | |
| { | |
| assert((cast(BitVector) _control).allAre1); | |
| return null; | |
| } | |
| auto i = (cast(BitVector)_control).findZeros(blocks, _startIdx * 64); | |
| if (i == i.max || i + blocks > _blocks) return null; | |
| // Allocate those bits | |
| (cast(BitVector) _control)[i .. i + blocks] = 1; | |
| return cast(void[]) _payload[cast(size_t) (i * blockSize) | |
| .. cast(size_t) ((i + blocks) * blockSize)]; | |
| } | |
| // Rounds sizeInBytes to a multiple of blockSize. | |
| private size_t bytes2blocks(size_t sizeInBytes) | |
| { | |
| return (sizeInBytes + blockSize - 1) / blockSize; | |
| } | |
| /* Allocates given blocks at the beginning blocks indicated by wordIdx. | |
| Returns true if allocation was possible, false otherwise. */ | |
| private bool allocateAtFront(size_t wordIdx, uint blocks) | |
| { | |
| assert(wordIdx < _control.rep.length && blocks >= 1 && blocks <= 64); | |
| const mask = (1UL << (64 - blocks)) - 1; | |
| if (_control.rep[wordIdx] > mask) return false; | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[wordIdx]; | |
| localControl |= ~mask; | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| _control.rep[wordIdx] |= ~mask; | |
| } | |
| return true; | |
| } | |
| // Since the lock is not pure, only the single threaded 'expand' is pure | |
| static if (isShared) | |
| { | |
| nothrow @trusted @nogc | |
| bool expand(ref void[] b, immutable size_t delta) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| return expandImpl(b, delta); | |
| } | |
| } | |
| else | |
| { | |
| pure nothrow @trusted @nogc | |
| bool expand(ref void[] b, immutable size_t delta) | |
| { | |
| return expandImpl(b, delta); | |
| } | |
| } | |
| // If shared, this is protected by a lock inside 'expand' | |
| pure nothrow @trusted @nogc | |
| private bool expandImpl(ref void[] b, immutable size_t delta) | |
| { | |
| // Dispose with trivial corner cases | |
| if (b is null || delta == 0) return delta == 0; | |
| /* To simplify matters, refuse to expand buffers that don't start at a block start (this may be the case for blocks allocated with alignedAllocate). | |
| */ | |
| if ((b.ptr - _payload.ptr) % blockSize) return false; | |
| const blocksOld = bytes2blocks(b.length); | |
| const blocksNew = bytes2blocks(b.length + delta); | |
| assert(blocksOld <= blocksNew); | |
| // Possibly we have enough slack at the end of the block! | |
| if (blocksOld == blocksNew) | |
| { | |
| b = b.ptr[0 .. b.length + delta]; | |
| return true; | |
| } | |
| assert((b.ptr - _payload.ptr) % blockSize == 0); | |
| const blockIdx = (b.ptr - _payload.ptr) / blockSize; | |
| const blockIdxAfter = blockIdx + blocksOld; | |
| // Try the maximum | |
| const wordIdx = blockIdxAfter / 64, | |
| msbIdx = cast(uint) (blockIdxAfter % 64); | |
| void[] p; | |
| auto hint = allocateAt(wordIdx, msbIdx, blocksNew - blocksOld, p); | |
| if (hint[0] != size_t.max) | |
| { | |
| return false; | |
| } | |
| // Expansion successful | |
| assert(p.ptr == b.ptr + blocksOld * blockSize); | |
| b = b.ptr[0 .. b.length + delta]; | |
| adjustFreshBit(blockIdx + blocksNew); | |
| return true; | |
| } | |
| @system bool reallocate(ref void[] b, size_t newSize) | |
| { | |
| static if (isShared) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| } | |
| return reallocateImpl(b, newSize); | |
| } | |
| // If shared, this is protected by a lock inside 'reallocate' | |
| private @system bool reallocateImpl(ref void[] b, size_t newSize) | |
| { | |
| static bool slowReallocate(Allocator)(ref Allocator a, ref void[] b, size_t s) | |
| { | |
| if (b.length == s) return true; | |
| if (b.length <= s && a.expandImpl(b, s - b.length)) return true; | |
| auto newB = a.allocateImpl(s); | |
| if (newB.length != s) return false; | |
| if (newB.length <= b.length) newB[] = b[0 .. newB.length]; | |
| else newB[0 .. b.length] = b[]; | |
| a.deallocateImpl(b); | |
| b = newB; | |
| return true; | |
| } | |
| if (!b.ptr) | |
| { | |
| b = allocateImpl(newSize); | |
| return b.length == newSize; | |
| } | |
| if (newSize == 0) | |
| { | |
| deallocateImpl(b); | |
| b = null; | |
| return true; | |
| } | |
| if (newSize < b.length) | |
| { | |
| // Shrink. Will shrink in place by deallocating the trailing part. | |
| auto newCapacity = bytes2blocks(newSize) * blockSize; | |
| deallocateImpl(b[newCapacity .. $]); | |
| b = b[0 .. newSize]; | |
| return true; | |
| } | |
| // Go the slow route | |
| return slowReallocate(this, b, newSize); | |
| } | |
| @system bool alignedReallocate(ref void[] b, size_t newSize, uint a) | |
| { | |
| static if (isShared) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| } | |
| return alignedReallocateImpl(b, newSize, a); | |
| } | |
| // If shared, this is protected by a lock inside 'alignedReallocate' | |
| private @system bool alignedReallocateImpl(ref void[] b, size_t newSize, uint a) | |
| { | |
| static bool slowAlignedReallocate(Allocator)(ref Allocator alloc, | |
| ref void[] b, size_t s, uint a) | |
| { | |
| if (b.length <= s && b.ptr.alignedAt(a) | |
| && alloc.expandImpl(b, s - b.length)) return true; | |
| auto newB = alloc.alignedAllocateImpl(s, a); | |
| if (newB.length != s) return false; | |
| if (newB.length <= b.length) newB[] = b[0 .. newB.length]; | |
| else newB[0 .. b.length] = b[]; | |
| alloc.deallocateImpl(b); | |
| b = newB; | |
| return true; | |
| } | |
| if (newSize == 0) | |
| { | |
| deallocateImpl(b); | |
| b = null; | |
| return true; | |
| } | |
| // Go the slow route | |
| return slowAlignedReallocate(this, b, newSize, a); | |
| } | |
| nothrow @nogc | |
| bool deallocate(void[] b) | |
| { | |
| static if (isShared) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| } | |
| return deallocateImpl(b); | |
| } | |
| // If shared, this is protected by a lock inside 'deallocate' | |
| nothrow @nogc | |
| private bool deallocateImpl(void[] b) | |
| { | |
| if (b is null) return true; | |
| // Locate position | |
| immutable pos = b.ptr - _payload.ptr; | |
| immutable blockIdx = pos / blockSize; | |
| // Adjust pointer, might be inside a block due to alignedAllocate | |
| void* begin = cast(void*) (_payload.ptr + blockIdx * blockSize), | |
| end = cast(void*) (b.ptr + b.length); | |
| b = begin[0 .. end - begin]; | |
| // Round up size to multiple of block size | |
| auto blocks = b.length.divideRoundUp(blockSize); | |
| // Get into details | |
| auto wordIdx = blockIdx / 64, msbIdx = cast(uint) (blockIdx % 64); | |
| if (_startIdx > wordIdx) _startIdx = wordIdx; | |
| // Three stages: heading bits, full words, leftover bits | |
| if (msbIdx) | |
| { | |
| if (blocks + msbIdx <= 64) | |
| { | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[wordIdx]; | |
| resetBits(localControl, | |
| cast(uint) (64 - msbIdx - blocks), | |
| 63 - msbIdx); | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| resetBits(_control.rep[wordIdx], | |
| cast(uint) (64 - msbIdx - blocks), | |
| 63 - msbIdx); | |
| } | |
| return true; | |
| } | |
| else | |
| { | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[wordIdx]; | |
| localControl &= ulong.max << (64 - msbIdx); | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| _control.rep[wordIdx] &= ulong.max << (64 - msbIdx); | |
| } | |
| blocks -= 64 - msbIdx; | |
| ++wordIdx; | |
| msbIdx = 0; | |
| } | |
| } | |
| // Stage 2: reset one word at a time | |
| for (; blocks >= 64; blocks -= 64) | |
| { | |
| _control.rep[wordIdx++] = 0; | |
| } | |
| // Stage 3: deal with leftover bits, if any | |
| assert(wordIdx <= _control.rep.length); | |
| if (blocks) | |
| { | |
| static if (isShared) | |
| { | |
| ulong localControl = _control.rep[wordIdx]; | |
| localControl &= ulong.max >> blocks; | |
| _control.rep[wordIdx] = localControl; | |
| } | |
| else | |
| { | |
| _control.rep[wordIdx] &= ulong.max >> blocks; | |
| } | |
| } | |
| return true; | |
| } | |
| // Since the lock is not pure, only the single threaded version is pure | |
| static if (isShared) | |
| { | |
| nothrow @nogc | |
| bool deallocateAll() | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| (cast(BitVector) _control)[] = 0; | |
| _startIdx = 0; | |
| return true; | |
| } | |
| } | |
| else | |
| { | |
| pure nothrow @nogc | |
| bool deallocateAll() | |
| { | |
| _control[] = 0; | |
| _startIdx = 0; | |
| return true; | |
| } | |
| } | |
| // Since the lock is not pure, only the single threaded version is pure | |
| static if (isShared) | |
| { | |
| nothrow @safe @nogc | |
| Ternary empty() | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| return emptyImpl(); | |
| } | |
| } | |
| else | |
| { | |
| pure nothrow @safe @nogc | |
| Ternary empty() | |
| { | |
| return Ternary(_control.allAre0()); | |
| } | |
| } | |
| pure nothrow @trusted @nogc | |
| private Ternary emptyImpl() | |
| { | |
| return Ternary((cast(BitVector) _control).allAre0()); | |
| } | |
| // Debug helper | |
| debug(StdBitmapped) | |
| private void dump() | |
| { | |
| import std.stdio : writefln, writeln; | |
| ulong controlLen = (cast(BitVector) _control).length; | |
| writefln("%s @ %s {", typeid(this), cast(void*) (cast(BitVector) _control)._rep.ptr); | |
| scope(exit) writeln("}"); | |
| assert(_payload.length >= blockSize * _blocks); | |
| assert(controlLen >= _blocks); | |
| writefln(" _startIdx=%s; blockSize=%s; blocks=%s", | |
| _startIdx, blockSize, _blocks); | |
| if (!controlLen) return; | |
| uint blockCount = 1; | |
| bool inAllocatedStore = (cast(BitVector) _control)[0]; | |
| void* start = cast(void*) _payload.ptr; | |
| for (size_t i = 1;; ++i) | |
| { | |
| if (i >= _blocks || (cast(BitVector) _control)[i] != inAllocatedStore) | |
| { | |
| writefln(" %s block at 0x%s, length: %s (%s*%s)", | |
| inAllocatedStore ? "Busy" : "Free", | |
| cast(void*) start, | |
| blockCount * blockSize, | |
| blockCount, blockSize); | |
| if (i >= _blocks) break; | |
| assert(i < controlLen); | |
| inAllocatedStore = (cast(BitVector) _control)[i]; | |
| start = cast(void*) (_payload.ptr + blockCount * blockSize); | |
| blockCount = 1; | |
| } | |
| else | |
| { | |
| ++blockCount; | |
| } | |
| } | |
| } | |
| void[] allocateAll() return scope | |
| { | |
| static if (isShared) | |
| { | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| } | |
| if (emptyImpl != Ternary.yes) return null; | |
| (cast(BitVector) _control)[] = 1; | |
| return cast(void[]) _payload; | |
| } | |
| } // Finish Yes.multiblock implementation specifics | |
| else | |
| { | |
| static if (isShared) | |
| pure nothrow @trusted @nogc | |
| void[] allocate(const size_t s) | |
| { | |
| import core.atomic : cas, atomicLoad, atomicOp; | |
| import core.bitop : bsr; | |
| import std.range : iota; | |
| import std.algorithm.iteration : map; | |
| import std.array : array; | |
| if (s.divideRoundUp(blockSize) != 1) | |
| return null; | |
| // First zero bit position for all values in the 0 - 255 range | |
| // for fast lookup | |
| static immutable ubyte[255] firstZero = iota(255U).map! | |
| (x => (7 - (bsr((~x) & 0x000000ff)))).array; | |
| foreach (size_t i; 0 .. _control.length) | |
| { | |
| ulong controlVal, newControlVal, bitIndex; | |
| do | |
| { | |
| bitIndex = 0; | |
| newControlVal = 0; | |
| controlVal = atomicLoad(_control[i]); | |
| // skip all control words which have all bits set | |
| if (controlVal == ulong.max) | |
| break; | |
| // fast lookup of first byte which has at least one zero bit | |
| foreach (byteIndex; 0 .. 8) | |
| { | |
| ulong mask = (0xFFUL << (8 * (7 - byteIndex))); | |
| if ((mask & controlVal) != mask) | |
| { | |
| ubyte byteVal = cast(ubyte) ((mask & controlVal) >> (8 * (7 - byteIndex))); | |
| bitIndex += firstZero[byteVal]; | |
| newControlVal = controlVal | (1UL << (63 - bitIndex)); | |
| break; | |
| } | |
| bitIndex += 8; | |
| } | |
| } while (!cas(&_control[i], controlVal, newControlVal)); | |
| auto blockIndex = bitIndex + 64 * i; | |
| if (controlVal != ulong.max && blockIndex < _blocks) | |
| { | |
| size_t payloadBlockStart = cast(size_t) blockIndex * blockSize; | |
| return cast(void[]) _payload[payloadBlockStart .. payloadBlockStart + s]; | |
| } | |
| } | |
| return null; | |
| } | |
| static if (!isShared) | |
| pure nothrow @trusted @nogc | |
| void[] allocate(const size_t s) | |
| { | |
| import core.bitop : bsr; | |
| import std.range : iota; | |
| import std.algorithm.iteration : map; | |
| import std.array : array; | |
| if (s.divideRoundUp(blockSize) != 1) | |
| return null; | |
| // First zero bit position for all values in the 0 - 255 range | |
| // for fast lookup | |
| static immutable ubyte[255] firstZero = iota(255U).map! | |
| (x => (7 - (bsr((~x) & 0x000000ff)))).array; | |
| _startIdx = (_startIdx + 1) % _control.length; | |
| foreach (size_t idx; 0 .. _control.length) | |
| { | |
| size_t i = (idx + _startIdx) % _control.length; | |
| size_t bitIndex = 0; | |
| // skip all control words which have all bits set | |
| if (_control[i] == ulong.max) | |
| continue; | |
| // fast lookup of first byte which has at least one zero bit | |
| foreach (byteIndex; 0 .. 8) | |
| { | |
| ulong mask = (0xFFUL << (8 * (7 - byteIndex))); | |
| if ((mask & _control[i]) != mask) | |
| { | |
| ubyte byteVal = cast(ubyte) ((mask & _control[i]) >> (8 * (7 - byteIndex))); | |
| bitIndex += firstZero[byteVal]; | |
| _control[i] |= (1UL << (63 - bitIndex)); | |
| break; | |
| } | |
| bitIndex += 8; | |
| } | |
| auto blockIndex = bitIndex + 64 * i; | |
| if (blockIndex < _blocks) | |
| { | |
| size_t payloadBlockStart = cast(size_t) blockIndex * blockSize; | |
| return cast(void[]) _payload[payloadBlockStart .. payloadBlockStart + s]; | |
| } | |
| } | |
| return null; | |
| } | |
| nothrow @nogc | |
| bool deallocate(void[] b) | |
| { | |
| static if (isShared) | |
| import core.atomic : atomicOp; | |
| if (b is null) | |
| return true; | |
| auto blockIndex = (b.ptr - _payload.ptr) / blockSize; | |
| auto controlIndex = blockIndex / 64; | |
| auto bitIndex = blockIndex % 64; | |
| static if (isShared) | |
| { | |
| atomicOp!"&="(_control[controlIndex], ~(1UL << (63 - bitIndex))); | |
| } | |
| else | |
| { | |
| _control[controlIndex] &= ~(1UL << (63 - bitIndex)); | |
| } | |
| return true; | |
| } | |
| pure nothrow @trusted @nogc | |
| bool expand(ref void[] b, immutable size_t delta) | |
| { | |
| if (delta == 0) | |
| return true; | |
| immutable newLength = delta + b.length; | |
| if (b is null || newLength > blockSize) | |
| return false; | |
| b = b.ptr[0 .. newLength]; | |
| return true; | |
| } | |
| } // Finish No.multiblock implementation specifics | |
| pure nothrow @trusted @nogc | |
| Ternary owns(const void[] b) const | |
| { | |
| assert(b || b.length == 0, "Corrupt block."); | |
| return Ternary(b && _payload && (&b[0] >= &_payload[0]) | |
| && (&b[0] + b.length) <= (&_payload[0] + _payload.length)); | |
| } | |
| } | |
| /** | |
| `BitmappedBlock` implements a simple heap consisting of one contiguous area | |
| of memory organized in blocks, each of size `theBlockSize`. A block is a unit | |
| of allocation. A bitmap serves as bookkeeping data, more precisely one bit per | |
| block indicating whether that block is currently allocated or not. | |
| Passing `NullAllocator` as `ParentAllocator` (the default) means user code | |
| manages allocation of the memory block from the outside; in that case | |
| `BitmappedBlock` must be constructed with a `ubyte[]` preallocated block and | |
| has no responsibility regarding the lifetime of its support underlying storage. | |
| If another allocator type is passed, `BitmappedBlock` defines a destructor that | |
| uses the parent allocator to release the memory block. That makes the combination of `AllocatorList`, | |
| `BitmappedBlock`, and a back-end allocator such as `MmapAllocator` | |
| a simple and scalable solution for memory allocation. | |
| There are advantages to storing bookkeeping data separated from the payload | |
| (as opposed to e.g. using `AffixAllocator` to store metadata together with | |
| each allocation). The layout is more compact (overhead is one bit per block), | |
| searching for a free block during allocation enjoys better cache locality, and | |
| deallocation does not touch memory around the payload being deallocated (which | |
| is often cold). | |
| Allocation requests are handled on a first-fit basis. Although linear in | |
| complexity, allocation is in practice fast because of the compact bookkeeping | |
| representation, use of simple and fast bitwise routines, and caching of the | |
| first available block position. A known issue with this general approach is | |
| fragmentation, partially mitigated by coalescing. Since `BitmappedBlock` does | |
| not need to maintain the allocated size, freeing memory implicitly coalesces | |
| free blocks together. Also, tuning `blockSize` has a considerable impact on | |
| both internal and external fragmentation. | |
| If the last template parameter is set to `No.multiblock`, the allocator will only serve | |
| allocations which require at most `theBlockSize`. The `BitmappedBlock` has a specialized | |
| implementation for single-block allocations which allows for greater performance, | |
| at the cost of not being able to allocate more than one block at a time. | |
| The size of each block can be selected either during compilation or at run | |
| time. Statically-known block sizes are frequent in practice and yield slightly | |
| better performance. To choose a block size statically, pass it as the `blockSize` | |
| parameter as in `BitmappedBlock!(4096)`. To choose a block | |
| size parameter, use `BitmappedBlock!(chooseAtRuntime)` and pass the | |
| block size to the constructor. | |
| Params: | |
| theBlockSize = the length of a block, which must be a multiple of `theAlignment` | |
| theAlignment = alignment of each block | |
| ParentAllocator = allocator from which the `BitmappedBlock` will draw memory. | |
| If set to `NullAllocator`, the storage must be passed via the constructor | |
| f = `Yes.multiblock` to support allocations spanning across multiple blocks and | |
| `No.multiblock` to support single block allocations. | |
| Although limited by single block allocations, `No.multiblock` will generally | |
| provide higher performance. | |
| */ | |
| struct BitmappedBlock(size_t theBlockSize, uint theAlignment = platformAlignment, | |
| ParentAllocator = NullAllocator, Flag!"multiblock" f = Yes.multiblock) | |
| { | |
| version (StdDdoc) | |
| { | |
| /** | |
| Constructs a block allocator given a hunk of memory, or a desired capacity | |
| in bytes. | |
| $(UL | |
| $(LI If `ParentAllocator` is $(REF_ALTTEXT `NullAllocator`, NullAllocator, std,experimental,allocator,building_blocks,null_allocator), | |
| only the constructor taking `data` is defined and the user is responsible for freeing `data` if desired.) | |
| $(LI Otherwise, both constructors are defined. The `data`-based | |
| constructor assumes memory has been allocated with the parent allocator. | |
| The `capacity`-based constructor uses `ParentAllocator` to allocate | |
| an appropriate contiguous hunk of memory. Regardless of the constructor | |
| used, the destructor releases the memory by using `ParentAllocator.deallocate`.) | |
| ) | |
| */ | |
| this(ubyte[] data); | |
| /// Ditto | |
| this(ubyte[] data, uint blockSize); | |
| /// Ditto | |
| this(size_t capacity); | |
| /// Ditto | |
| this(ParentAllocator parent, size_t capacity); | |
| /// Ditto | |
| this(size_t capacity, uint blockSize); | |
| /// Ditto | |
| this(ParentAllocator parent, size_t capacity, uint blockSize); | |
| /** | |
| If `blockSize == chooseAtRuntime`, `BitmappedBlock` offers a read/write | |
| property `blockSize`. It must be set before any use of the allocator. | |
| Otherwise (i.e. `theBlockSize` is a legit constant), `blockSize` is | |
| an alias for `theBlockSize`. Whether constant or variable, must also be | |
| a multiple of `alignment`. This constraint is `assert`ed statically | |
| and dynamically. | |
| */ | |
| alias blockSize = theBlockSize; | |
| /** | |
| The _alignment offered is user-configurable statically through parameter | |
| `theAlignment`, defaulted to `platformAlignment`. | |
| */ | |
| alias alignment = theAlignment; | |
| /** | |
| The _parent allocator. Depending on whether `ParentAllocator` holds state | |
| or not, this is a member variable or an alias for | |
| `ParentAllocator.instance`. | |
| */ | |
| ParentAllocator parent; | |
| /** | |
| Returns the actual bytes allocated when `n` bytes are requested, i.e. | |
| `n.roundUpToMultipleOf(blockSize)`. | |
| */ | |
| pure nothrow @safe @nogc | |
| size_t goodAllocSize(size_t n); | |
| /** | |
| Returns `Ternary.yes` if `b` belongs to the `BitmappedBlock` object, | |
| `Ternary.no` otherwise. Never returns `Ternary.unkown`. (This | |
| method is somewhat tolerant in that accepts an interior slice.) | |
| */ | |
| pure nothrow @trusted @nogc | |
| Ternary owns(const void[] b) const; | |
| /** | |
| Expands in place a buffer previously allocated by `BitmappedBlock`. | |
| If instantiated with `No.multiblock`, the expansion fails if the new length | |
| exceeds `theBlockSize`. | |
| */ | |
| pure nothrow @trusted @nogc | |
| bool expand(ref void[] b, immutable size_t delta); | |
| /** | |
| Deallocates a block previously allocated with this allocator. | |
| */ | |
| nothrow @nogc | |
| bool deallocate(void[] b); | |
| /** | |
| Allocates `s` bytes of memory and returns it, or `null` if memory | |
| could not be allocated. | |
| The following information might be of help with choosing the appropriate | |
| block size. Actual allocation occurs in sizes multiple of the block size. | |
| Allocating one block is the fastest because only one 0 bit needs to be | |
| found in the metadata. Allocating 2 through 64 blocks is the next cheapest | |
| because it affects a maximum of two `ulong` in the metadata. | |
| Allocations greater than 64 blocks require a multiword search through the | |
| metadata. | |
| If instantiated with `No.multiblock`, it performs a search for the first zero | |
| bit in the bitmap and sets it. | |
| */ | |
| pure nothrow @trusted @nogc | |
| void[] allocate(const size_t s); | |
| /** | |
| Allocates s bytes of memory and returns it, or `null` if memory could not be allocated. | |
| `allocateFresh` behaves just like allocate, the only difference being that this always | |
| returns unused(fresh) memory. Although there may still be available space in the `BitmappedBlock`, | |
| `allocateFresh` could still return null, because all the available blocks have been previously deallocated. | |
| */ | |
| @trusted void[] allocateFresh(const size_t s); | |
| /** | |
| If the `BitmappedBlock` object is empty (has no active allocation), allocates | |
| all memory within and returns a slice to it. Otherwise, returns `null` | |
| (i.e. no attempt is made to allocate the largest available block). | |
| */ | |
| void[] allocateAll(); | |
| /** | |
| Returns `Ternary.yes` if no memory is currently allocated with this | |
| allocator, otherwise `Ternary.no`. This method never returns | |
| `Ternary.unknown`. | |
| */ | |
| pure nothrow @safe @nogc | |
| Ternary empty(); | |
| /** | |
| Forcibly deallocates all memory allocated by this allocator, making it | |
| available for further allocations. Does not return memory to `ParentAllocator`. | |
| */ | |
| pure nothrow @nogc | |
| bool deallocateAll(); | |
| /** | |
| Reallocates a block previously allocated with `alignedAllocate`. Contractions do not occur in place. | |
| */ | |
| @system bool alignedReallocate(ref void[] b, size_t newSize, uint a); | |
| /** | |
| Reallocates a previously-allocated block. Contractions occur in place. | |
| */ | |
| @system bool reallocate(ref void[] b, size_t newSize); | |
| /** | |
| Allocates a block with specified alignment `a`. The alignment must be a | |
| power of 2. If `a <= alignment`, function forwards to `allocate`. | |
| Otherwise, it attempts to overallocate and then adjust the result for | |
| proper alignment. In the worst case the slack memory is around two blocks. | |
| */ | |
| void[] alignedAllocate(size_t n, uint a); | |
| /** | |
| If `ParentAllocator` is not `NullAllocator` and defines `deallocate`, | |
| the destructor is defined to deallocate the block held. | |
| */ | |
| ~this(); | |
| } | |
| else | |
| { | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| import std.algorithm.comparison : max; | |
| import std.experimental.allocator.mallocator : AlignedMallocator; | |
| auto m = cast(ubyte[])(AlignedMallocator.instance.alignedAllocate(1024 * 64, | |
| max(theAlignment, cast(uint) size_t.sizeof))); | |
| scope(exit) () nothrow @nogc { AlignedMallocator.instance.deallocate(m); }(); | |
| static if (theBlockSize == chooseAtRuntime) | |
| { | |
| testAllocator!(() => BitmappedBlock!(theBlockSize, theAlignment, NullAllocator)(m, 64)); | |
| } | |
| else | |
| { | |
| testAllocator!(() => BitmappedBlock!(theBlockSize, theAlignment, NullAllocator)(m)); | |
| } | |
| } | |
| mixin BitmappedBlockImpl!(false, f == Yes.multiblock); | |
| } | |
| } | |
| /// | |
| @system unittest | |
| { | |
| // Create a block allocator on top of a 10KB stack region. | |
| import std.experimental.allocator.building_blocks.region : InSituRegion; | |
| import std.traits : hasMember; | |
| InSituRegion!(10_240, 64) r; | |
| auto a = BitmappedBlock!(64, 64)(cast(ubyte[])(r.allocateAll())); | |
| static assert(hasMember!(InSituRegion!(10_240, 64), "allocateAll")); | |
| const b = a.allocate(100); | |
| assert(b.length == 100); | |
| } | |
| /// | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| import std.typecons : Flag, Yes; | |
| enum blockSize = 64; | |
| enum numBlocks = 10; | |
| // The 'BitmappedBlock' is implicitly instantiated with Yes.multiblock | |
| auto a = BitmappedBlock!(blockSize, 8, Mallocator, Yes.multiblock)(numBlocks * blockSize); | |
| // Instantiated with Yes.multiblock, can allocate more than one block at a time | |
| void[] buf = a.allocate(2 * blockSize); | |
| assert(buf.length == 2 * blockSize); | |
| assert(a.deallocate(buf)); | |
| // Can also allocate less than one block | |
| buf = a.allocate(blockSize / 2); | |
| assert(buf.length == blockSize / 2); | |
| // Expands inside the same block | |
| assert(a.expand(buf, blockSize / 2)); | |
| assert(buf.length == blockSize); | |
| // If Yes.multiblock, can expand past the size of a single block | |
| assert(a.expand(buf, 3 * blockSize)); | |
| assert(buf.length == 4 * blockSize); | |
| assert(a.deallocate(buf)); | |
| } | |
| /// | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| import std.typecons : Flag, No; | |
| enum blockSize = 64; | |
| auto a = BitmappedBlock!(blockSize, 8, Mallocator, No.multiblock)(1024 * blockSize); | |
| // Since instantiated with No.multiblock, can only allocate at most the block size | |
| void[] buf = a.allocate(blockSize + 1); | |
| assert(buf is null); | |
| buf = a.allocate(blockSize); | |
| assert(buf.length == blockSize); | |
| assert(a.deallocate(buf)); | |
| // This is also fine, because it's less than the block size | |
| buf = a.allocate(blockSize / 2); | |
| assert(buf.length == blockSize / 2); | |
| // Can expand the buffer until its length is at most 64 | |
| assert(a.expand(buf, blockSize / 2)); | |
| assert(buf.length == blockSize); | |
| // Cannot expand anymore | |
| assert(!a.expand(buf, 1)); | |
| assert(a.deallocate(buf)); | |
| } | |
| // Test instantiation with stateful allocators | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| import std.experimental.allocator.building_blocks.region : Region; | |
| auto r = Region!Mallocator(1024 * 96); | |
| auto a = BitmappedBlock!(chooseAtRuntime, 8, Region!Mallocator*, No.multiblock)(&r, 1024 * 64, 1024); | |
| } | |
| /** | |
| The threadsafe version of the $(LREF BitmappedBlock). | |
| The semantics of the `SharedBitmappedBlock` are identical to the regular $(LREF BitmappedBlock). | |
| Params: | |
| theBlockSize = the length of a block, which must be a multiple of `theAlignment` | |
| theAlignment = alignment of each block | |
| ParentAllocator = allocator from which the `BitmappedBlock` will draw memory. | |
| If set to `NullAllocator`, the storage must be passed via the constructor | |
| f = `Yes.multiblock` to support allocations spanning across multiple blocks and | |
| `No.multiblock` to support single block allocations. | |
| Although limited by single block allocations, `No.multiblock` will generally | |
| provide higher performance. | |
| */ | |
| shared struct SharedBitmappedBlock(size_t theBlockSize, uint theAlignment = platformAlignment, | |
| ParentAllocator = NullAllocator, Flag!"multiblock" f = Yes.multiblock) | |
| { | |
| version (StdDdoc) | |
| { | |
| /** | |
| Constructs a block allocator given a hunk of memory, or a desired capacity | |
| in bytes. | |
| $(UL | |
| $(LI If `ParentAllocator` is $(REF_ALTTEXT `NullAllocator`, NullAllocator, std,experimental,allocator,building_blocks,null_allocator), | |
| only the constructor taking `data` is defined and the user is responsible for freeing `data` if desired.) | |
| $(LI Otherwise, both constructors are defined. The `data`-based | |
| constructor assumes memory has been allocated with the parent allocator. | |
| The `capacity`-based constructor uses `ParentAllocator` to allocate | |
| an appropriate contiguous hunk of memory. Regardless of the constructor | |
| used, the destructor releases the memory by using `ParentAllocator.deallocate`.) | |
| ) | |
| */ | |
| this(ubyte[] data); | |
| /// Ditto | |
| this(ubyte[] data, uint blockSize); | |
| /// Ditto | |
| this(size_t capacity); | |
| /// Ditto | |
| this(ParentAllocator parent, size_t capacity); | |
| /// Ditto | |
| this(size_t capacity, uint blockSize); | |
| /// Ditto | |
| this(ParentAllocator parent, size_t capacity, uint blockSize); | |
| /** | |
| If `blockSize == chooseAtRuntime`, `SharedBitmappedBlock` offers a read/write | |
| property `blockSize`. It must be set before any use of the allocator. | |
| Otherwise (i.e. `theBlockSize` is a legit constant), `blockSize` is | |
| an alias for `theBlockSize`. Whether constant or variable, must also be | |
| a multiple of `alignment`. This constraint is `assert`ed statically | |
| and dynamically. | |
| */ | |
| alias blockSize = theBlockSize; | |
| /** | |
| The _alignment offered is user-configurable statically through parameter | |
| `theAlignment`, defaulted to `platformAlignment`. | |
| */ | |
| alias alignment = theAlignment; | |
| /** | |
| The _parent allocator. Depending on whether `ParentAllocator` holds state | |
| or not, this is a member variable or an alias for | |
| `ParentAllocator.instance`. | |
| */ | |
| ParentAllocator parent; | |
| /** | |
| Returns the actual bytes allocated when `n` bytes are requested, i.e. | |
| `n.roundUpToMultipleOf(blockSize)`. | |
| */ | |
| pure nothrow @safe @nogc | |
| size_t goodAllocSize(size_t n); | |
| /** | |
| Returns `Ternary.yes` if `b` belongs to the `SharedBitmappedBlock` object, | |
| `Ternary.no` otherwise. Never returns `Ternary.unkown`. (This | |
| method is somewhat tolerant in that accepts an interior slice.) | |
| */ | |
| pure nothrow @trusted @nogc | |
| Ternary owns(const void[] b) const; | |
| /** | |
| Expands in place a buffer previously allocated by `SharedBitmappedBlock`. | |
| Expansion fails if the new length exceeds the block size. | |
| */ | |
| bool expand(ref void[] b, immutable size_t delta); | |
| /** | |
| Deallocates the given buffer `b`, by atomically setting the corresponding | |
| bit to `0`. `b` must be valid, and cannot contain multiple adjacent `blocks`. | |
| */ | |
| nothrow @nogc | |
| bool deallocate(void[] b); | |
| /** | |
| Allocates `s` bytes of memory and returns it, or `null` if memory | |
| could not be allocated. | |
| The `SharedBitmappedBlock` cannot allocate more than the given block size. | |
| Allocations are satisfied by searching the first unset bit in the bitmap, | |
| and atomically setting it. | |
| In rare memory pressure scenarios, the allocation could fail. | |
| */ | |
| nothrow @trusted @nogc | |
| void[] allocate(const size_t s); | |
| /** | |
| Allocates s bytes of memory and returns it, or `null` if memory could not be allocated. | |
| `allocateFresh` behaves just like allocate, the only difference being that this always | |
| returns unused(fresh) memory. Although there may still be available space in the `SharedBitmappedBlock`, | |
| `allocateFresh` could still return null, because all the available blocks have been previously deallocated. | |
| */ | |
| @trusted void[] allocateFresh(const size_t s); | |
| /** | |
| If the `SharedBitmappedBlock` object is empty (has no active allocation), allocates | |
| all memory within and returns a slice to it. Otherwise, returns `null` | |
| (i.e. no attempt is made to allocate the largest available block). | |
| */ | |
| void[] allocateAll(); | |
| /** | |
| Returns `Ternary.yes` if no memory is currently allocated with this | |
| allocator, otherwise `Ternary.no`. This method never returns | |
| `Ternary.unknown`. | |
| */ | |
| nothrow @safe @nogc | |
| Ternary empty(); | |
| /** | |
| Forcibly deallocates all memory allocated by this allocator, making it | |
| available for further allocations. Does not return memory to `ParentAllocator`. | |
| */ | |
| nothrow @nogc | |
| bool deallocateAll(); | |
| /** | |
| Reallocates a block previously allocated with `alignedAllocate`. Contractions do not occur in place. | |
| */ | |
| @system bool alignedReallocate(ref void[] b, size_t newSize, uint a); | |
| /** | |
| Reallocates a previously-allocated block. Contractions occur in place. | |
| */ | |
| @system bool reallocate(ref void[] b, size_t newSize); | |
| /** | |
| Allocates a block with specified alignment `a`. The alignment must be a | |
| power of 2. If `a <= alignment`, function forwards to `allocate`. | |
| Otherwise, it attempts to overallocate and then adjust the result for | |
| proper alignment. In the worst case the slack memory is around two blocks. | |
| */ | |
| void[] alignedAllocate(size_t n, uint a); | |
| /** | |
| If `ParentAllocator` is not `NullAllocator` and defines `deallocate`, | |
| the destructor is defined to deallocate the block held. | |
| */ | |
| ~this(); | |
| } | |
| else | |
| { | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| import std.algorithm.comparison : max; | |
| import std.experimental.allocator.mallocator : AlignedMallocator; | |
| auto m = cast(ubyte[])(AlignedMallocator.instance.alignedAllocate(1024 * 64, | |
| max(theAlignment, cast(uint) size_t.sizeof))); | |
| scope(exit) () nothrow @nogc { AlignedMallocator.instance.deallocate(m); }(); | |
| static if (theBlockSize == chooseAtRuntime) | |
| { | |
| testAllocator!(() => SharedBitmappedBlock!(theBlockSize, theAlignment, NullAllocator)(m, 64)); | |
| } | |
| else | |
| { | |
| testAllocator!(() => SharedBitmappedBlock!(theBlockSize, theAlignment, NullAllocator)(m)); | |
| } | |
| } | |
| mixin BitmappedBlockImpl!(true, f == Yes.multiblock); | |
| } | |
| } | |
| /// | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| import std.experimental.allocator.common : platformAlignment; | |
| import std.typecons : Flag, Yes, No; | |
| // Create 'numThreads' threads, each allocating in parallel a chunk of memory | |
| static void testAlloc(Allocator)(ref Allocator a, size_t allocSize) | |
| { | |
| import core.thread : ThreadGroup; | |
| import std.algorithm.sorting : sort; | |
| import core.internal.spinlock : SpinLock; | |
| SpinLock lock = SpinLock(SpinLock.Contention.brief); | |
| enum numThreads = 10; | |
| void[][numThreads] buf; | |
| size_t count = 0; | |
| // Each threads allocates 'allocSize' | |
| void fun() | |
| { | |
| void[] b = a.allocate(allocSize); | |
| assert(b.length == allocSize); | |
| lock.lock(); | |
| scope(exit) lock.unlock(); | |
| buf[count] = b; | |
| count++; | |
| } | |
| auto tg = new ThreadGroup; | |
| foreach (i; 0 .. numThreads) | |
| { | |
| tg.create(&fun); | |
| } | |
| tg.joinAll(); | |
| // Sorting the allocations made by each thread, we expect the buffers to be | |
| // adjacent inside the SharedBitmappedBlock | |
| sort!((a, b) => a.ptr < b.ptr)(buf[0 .. numThreads]); | |
| foreach (i; 0 .. numThreads - 1) | |
| { | |
| assert(buf[i].ptr + a.goodAllocSize(buf[i].length) <= buf[i + 1].ptr); | |
| } | |
| // Deallocate everything | |
| foreach (i; 0 .. numThreads) | |
| { | |
| assert(a.deallocate(buf[i])); | |
| } | |
| } | |
| enum blockSize = 64; | |
| auto alloc1 = SharedBitmappedBlock!(blockSize, platformAlignment, Mallocator, Yes.multiblock)(1024 * 1024); | |
| auto alloc2 = SharedBitmappedBlock!(blockSize, platformAlignment, Mallocator, No.multiblock)(1024 * 1024); | |
| testAlloc(alloc1, 2 * blockSize); | |
| testAlloc(alloc2, blockSize); | |
| } | |
| @system unittest | |
| { | |
| // Test chooseAtRuntime | |
| // Create a block allocator on top of a 10KB stack region. | |
| import std.experimental.allocator.building_blocks.region : InSituRegion; | |
| import std.traits : hasMember; | |
| InSituRegion!(10_240, 64) r; | |
| uint blockSize = 64; | |
| auto a = BitmappedBlock!(chooseAtRuntime, 64)(cast(ubyte[])(r.allocateAll()), blockSize); | |
| static assert(hasMember!(InSituRegion!(10_240, 64), "allocateAll")); | |
| const b = (() pure nothrow @safe @nogc => a.allocate(100))(); | |
| assert(b.length == 100); | |
| } | |
| pure @safe unittest | |
| { | |
| import std.typecons : Ternary; | |
| auto a = (() @trusted => BitmappedBlock!(64, 64, NullAllocator, Yes.multiblock)(new ubyte[10_240]))(); | |
| () nothrow @nogc { | |
| assert(a.empty == Ternary.yes); | |
| const b = a.allocate(100); | |
| assert(b.length == 100); | |
| assert(a.empty == Ternary.no); | |
| }(); | |
| } | |
| @safe unittest | |
| { | |
| import std.typecons : Ternary; | |
| auto a = (() @trusted => SharedBitmappedBlock!(64, 64, NullAllocator, Yes.multiblock)(new ubyte[10_240]))(); | |
| assert((() nothrow @safe @nogc => a.empty)() == Ternary.yes); | |
| const b = a.allocate(100); | |
| assert(b.length == 100); | |
| } | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| import std.experimental.allocator.gc_allocator : GCAllocator; | |
| testAllocator!(() => BitmappedBlock!(64, 8, GCAllocator)(1024 * 64)); | |
| } | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| // Test chooseAtRuntime | |
| import std.experimental.allocator.gc_allocator : GCAllocator; | |
| uint blockSize = 64; | |
| testAllocator!(() => BitmappedBlock!(chooseAtRuntime, 8, GCAllocator, Yes.multiblock)(1024 * 64, blockSize)); | |
| testAllocator!(() => BitmappedBlock!(chooseAtRuntime, 8, GCAllocator, No.multiblock)(1024 * 64, blockSize)); | |
| } | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| testAllocator!(() => SharedBitmappedBlock!(64, 8, Mallocator, Yes.multiblock)(1024 * 64)); | |
| testAllocator!(() => SharedBitmappedBlock!(64, 8, Mallocator, No.multiblock)(1024 * 64)); | |
| } | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| // Test chooseAtRuntime | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| uint blockSize = 64; | |
| testAllocator!(() => SharedBitmappedBlock!(chooseAtRuntime, 8, Mallocator, Yes.multiblock)(1024 * 64, blockSize)); | |
| testAllocator!(() => SharedBitmappedBlock!(chooseAtRuntime, 8, Mallocator, No.multiblock)(1024 * 64, blockSize)); | |
| } | |
| @system unittest | |
| { | |
| static void testAllocateAll(size_t bs, bool isShared = true)(size_t blocks, uint blocksAtATime) | |
| { | |
| template attribAllocate(string size) | |
| { | |
| static if (isShared) | |
| { | |
| const char[] attribAllocate = "(() nothrow @safe @nogc => a.allocate(" ~ size ~ "))()"; | |
| } | |
| else | |
| { | |
| const char[] attribAllocate = "(() pure nothrow @safe @nogc => a.allocate(" ~ size ~ "))()"; | |
| } | |
| } | |
| assert(bs); | |
| import std.typecons : Ternary; | |
| import std.algorithm.comparison : min; | |
| import std.experimental.allocator.gc_allocator : GCAllocator; | |
| static if (isShared) | |
| { | |
| auto a = SharedBitmappedBlock!(bs, min(bs, platformAlignment), NullAllocator)( | |
| cast(ubyte[])(GCAllocator.instance.allocate((blocks * bs * 8 + blocks) / 8))); | |
| } | |
| else | |
| { | |
| auto a = BitmappedBlock!(bs, min(bs, platformAlignment), NullAllocator)( | |
| cast(ubyte[])(GCAllocator.instance.allocate((blocks * bs * 8 + blocks) / 8))); | |
| } | |
| import std.conv : text; | |
| assert(blocks >= a._blocks, text(blocks, " < ", a._blocks)); | |
| blocks = a._blocks; | |
| // test allocation of 0 bytes | |
| auto x = mixin(attribAllocate!("0")); | |
| assert(x is null); | |
| // test allocation of 1 byte | |
| x = mixin(attribAllocate!("1")); | |
| assert(x.length == 1 || blocks == 0); | |
| assert((() nothrow @nogc => a.deallocateAll())()); | |
| assert(a.empty() == Ternary.yes); | |
| bool twice = true; | |
| begin: | |
| foreach (i; 0 .. blocks / blocksAtATime) | |
| { | |
| auto b = mixin(attribAllocate!("bs * blocksAtATime")); | |
| assert(b.length == bs * blocksAtATime, text(i, ": ", b.length)); | |
| } | |
| assert(mixin(attribAllocate!("bs * blocksAtATime")) is null); | |
| if (a._blocks % blocksAtATime == 0) | |
| { | |
| assert(mixin(attribAllocate!("1")) is null); | |
| } | |
| // Now deallocate all and do it again! | |
| assert((() nothrow @nogc => a.deallocateAll())()); | |
| // Test deallocation | |
| auto v = new void[][blocks / blocksAtATime]; | |
| foreach (i; 0 .. blocks / blocksAtATime) | |
| { | |
| auto b = mixin(attribAllocate!("bs * blocksAtATime")); | |
| assert(b.length == bs * blocksAtATime, text(i, ": ", b.length)); | |
| v[i] = b; | |
| } | |
| assert(mixin(attribAllocate!("bs * blocksAtATime")) is null); | |
| if (a._blocks % blocksAtATime == 0) | |
| { | |
| assert(mixin(attribAllocate!("1")) is null); | |
| } | |
| foreach (i; 0 .. blocks / blocksAtATime) | |
| { | |
| () nothrow @nogc { a.deallocate(v[i]); }(); | |
| } | |
| foreach (i; 0 .. blocks / blocksAtATime) | |
| { | |
| auto b = mixin(attribAllocate!("bs * blocksAtATime")); | |
| assert(b.length == bs * blocksAtATime, text(i, ": ", b.length)); | |
| v[i] = b; | |
| } | |
| foreach (i; 0 .. v.length) | |
| { | |
| () nothrow @nogc { a.deallocate(v[i]); }(); | |
| } | |
| if (twice) | |
| { | |
| twice = false; | |
| goto begin; | |
| } | |
| assert((() nothrow @nogc => a.deallocateAll())()); | |
| // test expansion | |
| if (blocks >= blocksAtATime) | |
| { | |
| foreach (i; 0 .. blocks / blocksAtATime - 1) | |
| { | |
| auto b = mixin(attribAllocate!("bs * blocksAtATime")); | |
| assert(b.length == bs * blocksAtATime, text(i, ": ", b.length)); | |
| (cast(ubyte[]) b)[] = 0xff; | |
| static if (isShared) | |
| { | |
| assert((() nothrow @safe @nogc => a.expand(b, blocksAtATime * bs))() | |
| , text(i)); | |
| } | |
| else | |
| { | |
| assert((() pure nothrow @safe @nogc => a.expand(b, blocksAtATime * bs))() | |
| , text(i)); | |
| } | |
| (cast(ubyte[]) b)[] = 0xfe; | |
| assert(b.length == bs * blocksAtATime * 2, text(i, ": ", b.length)); | |
| a.reallocate(b, blocksAtATime * bs) || assert(0); | |
| assert(b.length == bs * blocksAtATime, text(i, ": ", b.length)); | |
| } | |
| } | |
| } | |
| testAllocateAll!(1)(0, 1); | |
| testAllocateAll!(1, false)(0, 1); | |
| testAllocateAll!(1)(8, 1); | |
| testAllocateAll!(1, false)(8, 1); | |
| testAllocateAll!(4096)(128, 1); | |
| testAllocateAll!(4096, false)(128, 1); | |
| testAllocateAll!(1)(0, 2); | |
| testAllocateAll!(1)(128, 2); | |
| testAllocateAll!(4096)(128, 2); | |
| testAllocateAll!(1, false)(0, 2); | |
| testAllocateAll!(1, false)(128, 2); | |
| testAllocateAll!(4096, false)(128, 2); | |
| testAllocateAll!(1)(0, 4); | |
| testAllocateAll!(1)(128, 4); | |
| testAllocateAll!(4096)(128, 4); | |
| testAllocateAll!(1, false)(0, 4); | |
| testAllocateAll!(1, false)(128, 4); | |
| testAllocateAll!(4096, false)(128, 4); | |
| testAllocateAll!(1)(0, 3); | |
| testAllocateAll!(1)(24, 3); | |
| testAllocateAll!(3008)(100, 1); | |
| testAllocateAll!(3008)(100, 3); | |
| testAllocateAll!(1, false)(0, 3); | |
| testAllocateAll!(1, false)(24, 3); | |
| testAllocateAll!(3008, false)(100, 1); | |
| testAllocateAll!(3008, false)(100, 3); | |
| testAllocateAll!(1)(0, 128); | |
| testAllocateAll!(1)(128 * 1, 128); | |
| testAllocateAll!(128 * 20)(13 * 128, 128); | |
| testAllocateAll!(1, false)(0, 128); | |
| testAllocateAll!(1, false)(128 * 1, 128); | |
| testAllocateAll!(128 * 20, false)(13 * 128, 128); | |
| } | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| enum blocks = 10000; | |
| int count = 0; | |
| ubyte[] payload = cast(ubyte[]) Mallocator.instance.allocate(blocks * 16); | |
| auto a = BitmappedBlock!(16, 16)(payload); | |
| void[][] buf = cast(void[][]) Mallocator.instance.allocate((void[]).sizeof * blocks); | |
| assert(!a.allocateFresh(0)); | |
| assert(!a._control[0]); | |
| void[] b = a.allocate(256 * 16); | |
| assert(b.length == 256 * 16); | |
| count += 256; | |
| assert(!a._control[count]); | |
| b = a.allocateFresh(16); | |
| assert(b.length == 16); | |
| count++; | |
| assert(a._control[count - 1]); | |
| b = a.allocateFresh(16 * 300); | |
| assert(b.length == 16 * 300); | |
| count += 300; | |
| for (int i = 0; i < count; i++) | |
| assert(a._control[i]); | |
| assert(!a._control[count]); | |
| assert(a.expand(b, 313 * 16)); | |
| count += 313; | |
| for (int i = 0; i < count; i++) | |
| assert(a._control[i]); | |
| assert(!a._control[count]); | |
| b = a.allocate(64 * 16); | |
| assert(b.length == 64 * 16); | |
| count += 64; | |
| b = a.allocateFresh(16); | |
| assert(b.length == 16); | |
| count++; | |
| for (int i = 0; i < count; i++) | |
| assert(a._control[i]); | |
| assert(!a._control[count]); | |
| assert(a.deallocateAll()); | |
| for (int i = 0; i < a._blocks; i++) | |
| assert(!a._control[i]); | |
| b = a.allocateFresh(257 * 16); | |
| assert(b.length == 257 * 16); | |
| for (int i = 0; i < count; i++) | |
| assert(!a._control[i]); | |
| for (int i = count; i < count + 257; i++) | |
| assert(a._control[i]); | |
| count += 257; | |
| assert(!a._control[count]); | |
| while (true) | |
| { | |
| b = a.allocate(16); | |
| if (!b) | |
| break; | |
| assert(b.length == 16); | |
| } | |
| assert(!a.allocateFresh(16)); | |
| assert(a.deallocateAll()); | |
| assert(a.allocate(16).length == 16); | |
| assert(!a.allocateFresh(16)); | |
| } | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| import std.random; | |
| static void testAlloc(Allocator)() | |
| { | |
| auto numBlocks = [1, 64, 256]; | |
| enum blocks = 10000; | |
| int iter = 0; | |
| ubyte[] payload = cast(ubyte[]) Mallocator.instance.allocate(blocks * 16); | |
| auto a = Allocator(payload); | |
| void[][] buf = cast(void[][]) Mallocator.instance.allocate((void[]).sizeof * blocks); | |
| auto rnd = Random(); | |
| while (iter < blocks) | |
| { | |
| int event = uniform(0, 2, rnd); | |
| int doExpand = uniform(0, 2, rnd); | |
| int allocSize = numBlocks[uniform(0, 3, rnd)] * 16; | |
| int expandSize = numBlocks[uniform(0, 3, rnd)] * 16; | |
| int doDeallocate = uniform(0, 2, rnd); | |
| if (event) buf[iter] = a.allocate(allocSize); | |
| else buf[iter] = a.allocateFresh(allocSize); | |
| if (!buf[iter]) | |
| break; | |
| assert(buf[iter].length == allocSize); | |
| auto oldSize = buf[iter].length; | |
| if (doExpand && a.expand(buf[iter], expandSize)) | |
| assert(buf[iter].length == expandSize + oldSize); | |
| if (doDeallocate) | |
| { | |
| assert(a.deallocate(buf[iter])); | |
| buf[iter] = null; | |
| } | |
| iter++; | |
| } | |
| while (iter < blocks) | |
| { | |
| buf[iter++] = a.allocate(16); | |
| if (!buf[iter - 1]) | |
| break; | |
| assert(buf[iter - 1].length == 16); | |
| } | |
| for (size_t i = 0; i < a._blocks; i++) | |
| assert((cast(BitVector) a._control)[i]); | |
| assert(!a.allocate(16)); | |
| for (size_t i = 0; i < iter; i++) | |
| { | |
| if (buf[i]) | |
| assert(a.deallocate(buf[i])); | |
| } | |
| for (size_t i = 0; i < a._blocks; i++) | |
| assert(!(cast(BitVector) a._control)[i]); | |
| } | |
| testAlloc!(BitmappedBlock!(16, 16))(); | |
| testAlloc!(SharedBitmappedBlock!(16, 16))(); | |
| } | |
| // Test totalAllocation and goodAllocSize | |
| nothrow @safe @nogc unittest | |
| { | |
| BitmappedBlock!(8, 8, NullAllocator) h1; | |
| assert(h1.goodAllocSize(1) == 8); | |
| assert(h1.totalAllocation(1) >= 8); | |
| assert(h1.totalAllocation(64) >= 64); | |
| assert(h1.totalAllocation(8 * 64) >= 8 * 64); | |
| assert(h1.totalAllocation(8 * 63) >= 8 * 63); | |
| assert(h1.totalAllocation(8 * 64 + 1) >= 8 * 65); | |
| BitmappedBlock!(64, 8, NullAllocator) h2; | |
| assert(h2.goodAllocSize(1) == 64); | |
| assert(h2.totalAllocation(1) >= 64); | |
| assert(h2.totalAllocation(64 * 64) >= 64 * 64); | |
| BitmappedBlock!(4096, 4096, NullAllocator) h3; | |
| assert(h3.goodAllocSize(1) == 4096); | |
| assert(h3.totalAllocation(1) >= 4096); | |
| assert(h3.totalAllocation(64 * 4096) >= 64 * 4096); | |
| assert(h3.totalAllocation(64 * 4096 + 1) >= 65 * 4096); | |
| } | |
| // Test owns | |
| @system unittest | |
| { | |
| import std.experimental.allocator.gc_allocator : GCAllocator; | |
| import std.typecons : Ternary; | |
| auto a = BitmappedBlock!(64, 8, GCAllocator)(1024 * 64); | |
| const void[] buff = (() pure nothrow @safe @nogc => a.allocate(42))(); | |
| assert((() nothrow @safe @nogc => a.owns(buff))() == Ternary.yes); | |
| assert((() nothrow @safe @nogc => a.owns(null))() == Ternary.no); | |
| } | |
| // BitmappedBlockWithInternalPointers | |
| /** | |
| A `BitmappedBlock` with additional structure for supporting `resolveInternalPointer`. | |
| To that end, `BitmappedBlockWithInternalPointers` adds a | |
| bitmap (one bit per block) that marks object starts. The bitmap itself has | |
| variable size and is allocated together with regular allocations. | |
| The time complexity of `resolveInternalPointer` is $(BIGOH k), where `k` | |
| is the size of the object within which the internal pointer is looked up. | |
| */ | |
| struct BitmappedBlockWithInternalPointers( | |
| size_t theBlockSize, uint theAlignment = platformAlignment, | |
| ParentAllocator = NullAllocator) | |
| { | |
| import std.conv : text; | |
| import std.typecons : Ternary; | |
| static if (!stateSize!ParentAllocator) | |
| version (StdUnittest) | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : AlignedMallocator; | |
| auto m = cast(ubyte[])(AlignedMallocator.instance.alignedAllocate(1024 * 64, | |
| theAlignment)); | |
| scope(exit) () nothrow @nogc { AlignedMallocator.instance.deallocate(m); }(); | |
| testAllocator!(() => BitmappedBlockWithInternalPointers(m)); | |
| } | |
| // state { | |
| private BitmappedBlock!(theBlockSize, theAlignment, ParentAllocator) _heap; | |
| private BitVector _allocStart; | |
| // } | |
| /** | |
| Constructors accepting desired capacity or a preallocated buffer, similar | |
| in semantics to those of `BitmappedBlock`. | |
| */ | |
| static if (!stateSize!ParentAllocator) | |
| this(ubyte[] data) | |
| { | |
| _heap = BitmappedBlock!(theBlockSize, theAlignment, ParentAllocator)(data); | |
| } | |
| static if (stateSize!ParentAllocator) | |
| this(ParentAllocator parent, ubyte[] data) | |
| { | |
| _heap = BitmappedBlock!(theBlockSize, theAlignment, ParentAllocator)(data); | |
| _heap.parent = parent; | |
| } | |
| /// Ditto | |
| static if (!is(ParentAllocator == NullAllocator) && !stateSize!ParentAllocator) | |
| this(size_t capacity) | |
| { | |
| // Add room for the _allocStart vector | |
| _heap = BitmappedBlock!(theBlockSize, theAlignment, ParentAllocator) | |
| (capacity + capacity.divideRoundUp(64)); | |
| } | |
| /// Ditto | |
| static if (!is(ParentAllocator == NullAllocator) && stateSize!ParentAllocator) | |
| this(ParentAllocator parent, size_t capacity) | |
| { | |
| // Add room for the _allocStart vector | |
| _heap = BitmappedBlock!(theBlockSize, theAlignment, ParentAllocator) | |
| (parent, capacity + capacity.divideRoundUp(64)); | |
| } | |
| // Makes sure there's enough room for _allocStart | |
| @safe | |
| private bool ensureRoomForAllocStart(size_t len) | |
| { | |
| if (_allocStart.length >= len) return true; | |
| // Must ensure there's room | |
| immutable oldLength = _allocStart.rep.length; | |
| immutable bits = len.roundUpToMultipleOf(64); | |
| void[] b = _allocStart.rep; | |
| if ((() @trusted => !_heap.reallocate(b, bits / 8))()) return false; | |
| assert(b.length * 8 == bits); | |
| _allocStart = BitVector((() @trusted => cast(ulong[]) b)()); | |
| assert(_allocStart.rep.length * 64 == bits); | |
| _allocStart.rep[oldLength .. $] = ulong.max; | |
| return true; | |
| } | |
| /** | |
| Allocator primitives. | |
| */ | |
| alias alignment = theAlignment; | |
| /// Ditto | |
| pure nothrow @safe @nogc | |
| size_t goodAllocSize(size_t n) | |
| { | |
| return n.roundUpToMultipleOf(_heap.blockSize); | |
| } | |
| /// Ditto | |
| void[] allocate(size_t bytes) | |
| { | |
| auto r = _heap.allocate(bytes); | |
| if (!r.ptr) return r; | |
| immutable block = (() @trusted => (r.ptr - _heap._payload.ptr) / _heap.blockSize)(); | |
| immutable blocks = | |
| (r.length + _heap.blockSize - 1) / _heap.blockSize; | |
| if (!ensureRoomForAllocStart(block + blocks)) | |
| { | |
| // Failed, free r and bailout | |
| () @trusted { _heap.deallocate(r); r = null; }(); | |
| return null; | |
| } | |
| assert(block < _allocStart.length); | |
| assert(block + blocks <= _allocStart.length); | |
| // Mark the _allocStart bits | |
| assert(blocks > 0); | |
| _allocStart[block] = 1; | |
| _allocStart[block + 1 .. block + blocks] = 0; | |
| assert(block + blocks == _allocStart.length | |
| || _allocStart[block + blocks] == 1); | |
| return r; | |
| } | |
| /// Ditto | |
| void[] allocateAll() | |
| { | |
| auto r = _heap.allocateAll(); | |
| if (!r.ptr) return r; | |
| // Carve space at the end for _allocStart | |
| auto p = alignDownTo(r.ptr + r.length - 8, ulong.alignof); | |
| r = r[0 .. p - r.ptr]; | |
| // Initialize _allocStart | |
| _allocStart = BitVector(cast(ulong[]) p[0 .. 8]); | |
| _allocStart[] = 0; | |
| immutable block = (r.ptr - _heap._payload.ptr) / _heap.blockSize; | |
| assert(block < _allocStart.length); | |
| _allocStart[block] = 1; | |
| return r; | |
| } | |
| /// Ditto | |
| bool expand(ref void[] b, size_t bytes) | |
| { | |
| if (!bytes) return true; | |
| if (b is null) return false; | |
| immutable oldBlocks = | |
| (b.length + _heap.blockSize - 1) / _heap.blockSize; | |
| assert(oldBlocks); | |
| immutable newBlocks = | |
| (b.length + bytes + _heap.blockSize - 1) / _heap.blockSize; | |
| assert(newBlocks >= oldBlocks); | |
| immutable block = (() @trusted => (b.ptr - _heap._payload.ptr) / _heap.blockSize)(); | |
| assert(_allocStart[block]); | |
| if (!ensureRoomForAllocStart(block + newBlocks) | |
| || !_heap.expand(b, bytes)) | |
| { | |
| return false; | |
| } | |
| // Zero only the expanded bits | |
| _allocStart[block + oldBlocks .. block + newBlocks] = 0; | |
| assert(_allocStart[block]); | |
| return true; | |
| } | |
| /// Ditto | |
| bool deallocate(void[] b) | |
| { | |
| // No need to touch _allocStart here - except for the first bit, it's | |
| // meaningless in freed memory. The first bit is already 1. | |
| return _heap.deallocate(b); | |
| // TODO: one smart thing to do is reduce memory occupied by | |
| // _allocStart if we're freeing the rightmost block. | |
| } | |
| /// Ditto | |
| nothrow @safe @nogc | |
| Ternary resolveInternalPointer(const void* p, ref void[] result) | |
| { | |
| if ((() @trusted => _heap._payload | |
| && (p < &_heap._payload[0] | |
| || p >= &_heap._payload[0] + _heap._payload.length))()) | |
| { | |
| return Ternary.no; | |
| } | |
| // Find block start | |
| auto block = (() @trusted => (p - &_heap._payload[0]) / _heap.blockSize)(); | |
| if (block >= _allocStart.length) return Ternary.no; | |
| // Within an allocation, must find the 1 just to the left of it | |
| auto i = _allocStart.find1Backward(block); | |
| if (i == i.max) return Ternary.no; | |
| auto j = _allocStart.find1(i + 1); | |
| result = (() @trusted => _heap._payload.ptr[cast(size_t) (_heap.blockSize * i) | |
| .. cast(size_t) (_heap.blockSize * j)])(); | |
| return Ternary.yes; | |
| } | |
| /// Ditto | |
| Ternary empty() | |
| { | |
| return _heap.empty; | |
| } | |
| // Currently unused | |
| private void markAllAsUnused() | |
| { | |
| // Mark all deallocated memory with 1 so we minimize damage created by | |
| // false pointers. TODO: improve speed. | |
| foreach (i, ref e; _allocStart.rep) | |
| { | |
| // Set to 1 all bits in _allocStart[i] that were 0 in control, and | |
| // leave the others unchanged. | |
| // (0, 0) => 1; (0, 1) => 0; (1, 0) => 1; (1, 1) => 1 | |
| e |= ~_heap._control.rep[i]; | |
| } | |
| // Now zero all control bits | |
| _heap._control[] = 0; | |
| // EXCEPT for the _allocStart block itself | |
| markAsUsed(_allocStart.rep); | |
| } | |
| // Currently unused | |
| private bool markAsUsed(void[] b) | |
| { | |
| // Locate position | |
| immutable pos = b.ptr - _heap._payload.ptr; | |
| assert(pos % _heap.blockSize == 0); | |
| auto blockIdx = pos / _heap.blockSize; | |
| if (_heap._control[blockIdx]) return false; | |
| // Round up size to multiple of block size | |
| auto blocks = b.length.divideRoundUp(_heap.blockSize); | |
| _heap._control[blockIdx .. blockIdx + blocks] = 1; | |
| return true; | |
| } | |
| // Currently unused | |
| private void doneMarking() | |
| { | |
| // Nothing to do, what's free stays free. | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.typecons : Ternary; | |
| auto h = BitmappedBlockWithInternalPointers!(4096)(new ubyte[4096 * 1024]); | |
| assert((() nothrow @safe @nogc => h.empty)() == Ternary.yes); | |
| auto b = (() pure nothrow @safe @nogc => h.allocate(123))(); | |
| assert(b.length == 123); | |
| assert((() nothrow @safe @nogc => h.empty)() == Ternary.no); | |
| void[] p; | |
| void* offset = &b[0] + 17; | |
| assert((() nothrow @safe @nogc => h.resolveInternalPointer(offset, p))() == Ternary.yes); | |
| assert(p.ptr is b.ptr); | |
| assert(p.length >= b.length); | |
| b = (() pure nothrow @safe @nogc => h.allocate(4096))(); | |
| offset = &b[0]; | |
| assert((() nothrow @safe @nogc => h.resolveInternalPointer(offset, p))() == Ternary.yes); | |
| assert(p is b); | |
| offset = &b[0] + 11; | |
| assert((() nothrow @safe @nogc => h.resolveInternalPointer(offset, p))() == Ternary.yes); | |
| assert(p is b); | |
| void[] unchanged = p; | |
| offset = &b[0] - 40_970; | |
| assert((() nothrow @safe @nogc => h.resolveInternalPointer(offset, p))() == Ternary.no); | |
| assert(p is unchanged); | |
| assert((() @safe => h.expand(b, 1))()); | |
| assert(b.length == 4097); | |
| offset = &b[0] + 4096; | |
| assert((() nothrow @safe @nogc => h.resolveInternalPointer(offset, p))() == Ternary.yes); | |
| assert(p.ptr is b.ptr); | |
| // Ensure deallocate inherits from parent | |
| () nothrow @nogc { h.deallocate(b); }(); | |
| } | |
| @system unittest | |
| { | |
| auto h = BitmappedBlockWithInternalPointers!(4096)(new ubyte[4096 * 1024]); | |
| assert((() pure nothrow @safe @nogc => h.goodAllocSize(1))() == 4096); | |
| } | |
| // Test instantiation with stateful allocators | |
| @system unittest | |
| { | |
| import std.experimental.allocator.mallocator : Mallocator; | |
| import std.experimental.allocator.building_blocks.region : Region; | |
| auto r = Region!Mallocator(1024 * 1024); | |
| auto h = BitmappedBlockWithInternalPointers!(4096, 8, Region!Mallocator*)(&r, 4096 * 1024); | |
| } | |
| /** | |
| Returns the number of most significant ones before a zero can be found in `x`. | |
| If `x` contains no zeros (i.e. is equal to `ulong.max`), returns 64. | |
| */ | |
| pure nothrow @safe @nogc | |
| private uint leadingOnes(ulong x) | |
| { | |
| import core.bitop : bsr; | |
| const x_ = ~x; | |
| return x_ == 0 ? 64 : (63 - bsr(x_)); | |
| } | |
| @safe unittest | |
| { | |
| assert(leadingOnes(0) == 0); | |
| assert(leadingOnes(~0UL) == 64); | |
| assert(leadingOnes(0xF000_0000_0000_0000) == 4); | |
| assert(leadingOnes(0xE400_0000_0000_0000) == 3); | |
| assert(leadingOnes(0xC700_0200_0000_0000) == 2); | |
| assert(leadingOnes(0x8000_0030_0000_0000) == 1); | |
| assert(leadingOnes(0x2000_0000_0000_0000) == 0); | |
| } | |
| /** | |
| Finds a run of contiguous ones in `x` of length at least `n`. | |
| */ | |
| pure nothrow @safe @nogc | |
| private uint findContigOnes(ulong x, uint n) | |
| { | |
| while (n > 1) | |
| { | |
| immutable s = n >> 1; | |
| x &= x << s; | |
| n -= s; | |
| } | |
| return leadingOnes(~x); | |
| } | |
| @safe unittest | |
| { | |
| assert(findContigOnes(0x0000_0000_0000_0300, 2) == 54); | |
| assert(findContigOnes(~0UL, 1) == 0); | |
| assert(findContigOnes(~0UL, 2) == 0); | |
| assert(findContigOnes(~0UL, 32) == 0); | |
| assert(findContigOnes(~0UL, 64) == 0); | |
| assert(findContigOnes(0UL, 1) == 64); | |
| assert(findContigOnes(0x4000_0000_0000_0000, 1) == 1); | |
| assert(findContigOnes(0x0000_0F00_0000_0000, 4) == 20); | |
| } | |
| /* | |
| Unconditionally sets the bits from lsb through msb in w to zero. | |
| */ | |
| pure nothrow @safe @nogc | |
| private void setBits(ref ulong w, uint lsb, uint msb) | |
| { | |
| assert(lsb <= msb && msb < 64); | |
| const mask = (ulong.max << lsb) & (ulong.max >> (63 - msb)); | |
| w |= mask; | |
| } | |
| @safe unittest | |
| { | |
| ulong w; | |
| w = 0; setBits(w, 0, 63); assert(w == ulong.max); | |
| w = 0; setBits(w, 1, 63); assert(w == ulong.max - 1); | |
| w = 6; setBits(w, 0, 1); assert(w == 7); | |
| w = 6; setBits(w, 3, 3); assert(w == 14); | |
| } | |
| /* Are bits from lsb through msb in w zero? If so, make then 1 | |
| and return the resulting w. Otherwise, just return 0. | |
| */ | |
| pure nothrow @safe @nogc | |
| private bool setBitsIfZero(ref ulong w, uint lsb, uint msb) | |
| { | |
| assert(lsb <= msb && msb < 64); | |
| const mask = (ulong.max << lsb) & (ulong.max >> (63 - msb)); | |
| if (w & mask) return false; | |
| w |= mask; | |
| return true; | |
| } | |
| // Assigns bits in w from lsb through msb to zero. | |
| pure nothrow @safe @nogc | |
| private void resetBits(ref ulong w, uint lsb, uint msb) | |
| { | |
| assert(lsb <= msb && msb < 64); | |
| const mask = (ulong.max << lsb) & (ulong.max >> (63 - msb)); | |
| w &= ~mask; | |
| } | |
| /* | |
| Bit disposition is MSB=0 (leftmost, big endian). | |
| */ | |
| private struct BitVector | |
| { | |
| ulong[] _rep; | |
| auto rep(this _)() { return _rep; } | |
| pure nothrow @safe @nogc | |
| this(ulong[] data) { _rep = data; } | |
| pure nothrow @safe @nogc | |
| void opSliceAssign(bool b) { _rep[] = b ? ulong.max : 0; } | |
| pure nothrow @safe @nogc | |
| void opSliceAssign(bool b, ulong x, ulong y) | |
| { | |
| assert(x <= y && y <= _rep.length * 64); | |
| if (x == y) return; | |
| --y; | |
| assert(x / 64 <= size_t.max); | |
| immutable i1 = cast(size_t) (x / 64); | |
| immutable uint b1 = 63 - x % 64; | |
| assert(y / 64 <= size_t.max); | |
| immutable i2 = cast(size_t) (y / 64); | |
| immutable uint b2 = 63 - y % 64; | |
| assert(i1 <= i2 && i2 < _rep.length); | |
| if (i1 == i2) | |
| { | |
| // Inside the same word | |
| assert(b1 >= b2); | |
| if (b) setBits(_rep[i1], b2, b1); | |
| else resetBits(_rep[i1], b2, b1); | |
| } | |
| else | |
| { | |
| // Spans multiple words | |
| assert(i1 < i2); | |
| if (b) setBits(_rep[i1], 0, b1); | |
| else resetBits(_rep[i1], 0, b1); | |
| _rep[i1 + 1 .. i2] = (b ? ulong.max : 0); | |
| if (b) setBits(_rep[i2], b2, 63); | |
| else resetBits(_rep[i2], b2, 63); | |
| } | |
| } | |
| pure nothrow @safe @nogc | |
| bool opIndex(ulong x) | |
| { | |
| assert(x < length); | |
| return (_rep[cast(size_t) (x / 64)] | |
| & (0x8000_0000_0000_0000UL >> (x % 64))) != 0; | |
| } | |
| pure nothrow @safe @nogc | |
| void opIndexAssign(bool b, ulong x) | |
| { | |
| assert(x / 64 <= size_t.max); | |
| immutable i = cast(size_t) (x / 64); | |
| immutable j = 0x8000_0000_0000_0000UL >> (x % 64); | |
| if (b) _rep[i] |= j; | |
| else _rep[i] &= ~j; | |
| } | |
| pure nothrow @safe @nogc | |
| ulong length() const | |
| { | |
| return _rep.length * 64; | |
| } | |
| /* Returns the index of the first 1 to the right of i (including i itself), | |
| or length if not found. | |
| */ | |
| pure nothrow @safe @nogc | |
| ulong find1(ulong i) | |
| { | |
| assert(i < length); | |
| assert(i / 64 <= size_t.max); | |
| auto w = cast(size_t) (i / 64); | |
| immutable b = i % 64; // 0 through 63, 0 when i == 0 | |
| immutable mask = ulong.max >> b; | |
| if (auto current = _rep[w] & mask) | |
| { | |
| // Great, found | |
| return w * 64 + leadingOnes(~current); | |
| } | |
| // The current word doesn't have the solution, find the leftmost 1 | |
| // going to the right. | |
| for (++w; w < _rep.length; ++w) | |
| { | |
| if (auto current = _rep[w]) | |
| { | |
| return w * 64 + leadingOnes(~current); | |
| } | |
| } | |
| return length; | |
| } | |
| /* Returns the index of the first 1 to the left of i (including i itself), | |
| or ulong.max if not found. | |
| */ | |
| pure nothrow @safe @nogc | |
| ulong find1Backward(ulong i) | |
| { | |
| assert(i < length); | |
| auto w = cast(size_t) (i / 64); | |
| immutable b = 63 - (i % 64); // 0 through 63, 63 when i == 0 | |
| immutable mask = ~((1UL << b) - 1); | |
| assert(mask != 0); | |
| // First, let's see if the current word has a bit larger than ours. | |
| if (auto currentWord = _rep[w] & mask) | |
| { | |
| // Great, this word contains the result. | |
| return w * 64 + 63 - currentWord.trailingZeros; | |
| } | |
| // The current word doesn't have the solution, find the rightmost 1 | |
| // going to the left. | |
| while (w >= 1) | |
| { | |
| --w; | |
| if (auto currentWord = _rep[w]) | |
| return w * 64 + (63 - currentWord.trailingZeros); | |
| } | |
| return ulong.max; | |
| } | |
| /// Are all bits zero? | |
| pure nothrow @safe @nogc | |
| bool allAre0() const | |
| { | |
| foreach (w; _rep) if (w) return false; | |
| return true; | |
| } | |
| /// Are all bits one? | |
| pure nothrow @safe @nogc | |
| bool allAre1() const | |
| { | |
| foreach (w; _rep) if (w != ulong.max) return false; | |
| return true; | |
| } | |
| pure nothrow @safe @nogc | |
| ulong findZeros(immutable size_t howMany, ulong start) | |
| { | |
| assert(start < length); | |
| assert(howMany > 64); | |
| auto i = cast(size_t) (start / 64); | |
| while (_rep[i] & 1) | |
| { | |
| // No trailing zeros in this word, try the next one | |
| if (++i == _rep.length) return ulong.max; | |
| start = i * 64; | |
| } | |
| // Adjust start to have only trailing zeros after it | |
| auto prefixLength = 64; | |
| while (_rep[i] & (ulong.max >> (64 - prefixLength))) | |
| { | |
| assert(prefixLength > 0); | |
| --prefixLength; | |
| ++start; | |
| } | |
| assert(howMany > prefixLength); | |
| auto needed = howMany - prefixLength; | |
| for (++i; needed >= 64; needed -= 64, ++i) | |
| { | |
| if (i >= _rep.length) return ulong.max; | |
| if (_rep[i] != 0) return findZeros(howMany, i * 64); | |
| } | |
| // Leftover < 64 bits | |
| assert(needed < 64); | |
| if (!needed) return start; | |
| if (i >= _rep.length) return ulong.max; | |
| if (leadingOnes(~_rep[i]) >= needed) return start; | |
| return findZeros(howMany, i * 64); | |
| } | |
| } | |
| @safe unittest | |
| { | |
| auto v = BitVector(new ulong[10]); | |
| assert(v.length == 640); | |
| v[] = 0; | |
| v[53] = 1; | |
| assert(v[52] == 0); | |
| assert(v[53] == 1); | |
| assert(v[54] == 0); | |
| v[] = 0; | |
| v[53 .. 55] = 1; | |
| assert(v[52] == 0); | |
| assert(v[53] == 1); | |
| assert(v[54] == 1); | |
| assert(v[55] == 0); | |
| v[] = 0; | |
| v[2 .. 65] = 1; | |
| assert(v.rep[0] == 0x3FFF_FFFF_FFFF_FFFF); | |
| assert(v.rep[1] == 0x8000_0000_0000_0000); | |
| assert(v.rep[2] == 0); | |
| v[] = 0; | |
| assert(v.find1Backward(0) == ulong.max); | |
| assert(v.find1Backward(43) == ulong.max); | |
| assert(v.find1Backward(83) == ulong.max); | |
| v[0] = 1; | |
| assert(v.find1Backward(0) == 0); | |
| assert(v.find1Backward(43) == 0); | |
| import std.conv : text; | |
| assert(v.find1Backward(83) == 0, text(v.find1Backward(83))); | |
| v[0] = 0; | |
| v[101] = 1; | |
| assert(v.find1Backward(0) == ulong.max); | |
| assert(v.find1Backward(43) == ulong.max); | |
| assert(v.find1Backward(83) == ulong.max); | |
| assert(v.find1Backward(100) == ulong.max); | |
| assert(v.find1Backward(101) == 101); | |
| assert(v.find1Backward(553) == 101); | |
| v[0 .. v.length] = 0; | |
| v[v.length .. v.length] = 0; | |
| v[0 .. 0] = 0; | |
| v[] = 0; | |
| assert(v.find1(0) == v.length); | |
| v[139] = 1; | |
| assert(v.find1(0) == 139); | |
| assert(v.find1(100) == 139); | |
| assert(v.find1(138) == 139); | |
| assert(v.find1(139) == 139); | |
| assert(v.find1(140) == v.length); | |
| v[] = 0; | |
| assert(v.findZeros(100, 0) == 0); | |
| foreach (i; 0 .. 500) | |
| assert(v.findZeros(100, i) == i, text(v.findZeros(100, i), " != ", i)); | |
| assert(v.findZeros(540, 99) == 99); | |
| assert(v.findZeros(99, 540) == 540); | |
| assert(v.findZeros(540, 100) == 100); | |
| assert(v.findZeros(640, 0) == 0); | |
| assert(v.findZeros(641, 1) == ulong.max); | |
| assert(v.findZeros(641, 100) == ulong.max); | |
| } |