Keep a scratch block around for reuse in GrBlockAllocator
This can optimize cases in a allocate-release loop that moves back and forth across the end of one block and the start of another. Before this would malloc a new block and then delete it. It also enables reserve() in higher-level data collections w/o blocking bytes left in the current tail block. Change-Id: Ide16e9038384fcb188164fc9620a8295f6880b9f Reviewed-on: https://skia-review.googlesource.com/c/skia/+/303268 Reviewed-by: Brian Salomon <bsalomon@google.com> Reviewed-by: Robert Phillips <robertphillips@google.com> Commit-Queue: Michael Ludwig <michaelludwig@google.com>
This commit is contained in:
parent
dc945ea077
commit
68e5f29d84
@ -46,7 +46,7 @@ GrBlockAllocator::Block::~Block() {
|
||||
|
||||
size_t GrBlockAllocator::totalSize() const {
|
||||
// Use size_t since the sum across all blocks could exceed 'int', even though each block won't
|
||||
size_t size = offsetof(GrBlockAllocator, fHead);
|
||||
size_t size = offsetof(GrBlockAllocator, fHead) + this->scratchBlockSize();
|
||||
for (const Block* b : this->blocks()) {
|
||||
size += b->fSize;
|
||||
}
|
||||
@ -55,7 +55,10 @@ size_t GrBlockAllocator::totalSize() const {
|
||||
}
|
||||
|
||||
size_t GrBlockAllocator::totalUsableSpace() const {
|
||||
size_t size = 0;
|
||||
size_t size = this->scratchBlockSize();
|
||||
if (size > 0) {
|
||||
size -= kDataStart; // scratchBlockSize reports total block size, not usable size
|
||||
}
|
||||
for (const Block* b : this->blocks()) {
|
||||
size += (b->fSize - kDataStart);
|
||||
}
|
||||
@ -87,9 +90,14 @@ GrBlockAllocator::Block* GrBlockAllocator::findOwningBlock(const void* p) {
|
||||
}
|
||||
|
||||
void GrBlockAllocator::releaseBlock(Block* block) {
|
||||
if (block->fPrev) {
|
||||
// Unlink block from the double-linked list of blocks
|
||||
SkASSERT(block != &fHead);
|
||||
if (block == &fHead) {
|
||||
// Reset the cursor of the head block so that it can be reused if it becomes the new tail
|
||||
block->fCursor = kDataStart;
|
||||
block->fMetadata = 0;
|
||||
// Unlike in reset(), we don't set the head's next block to null because there are
|
||||
// potentially heap-allocated blocks that are still connected to it.
|
||||
} else {
|
||||
SkASSERT(block->fPrev);
|
||||
block->fPrev->fNext = block->fNext;
|
||||
if (block->fNext) {
|
||||
SkASSERT(fTail != block);
|
||||
@ -99,14 +107,17 @@ void GrBlockAllocator::releaseBlock(Block* block) {
|
||||
fTail = block->fPrev;
|
||||
}
|
||||
|
||||
delete block;
|
||||
} else {
|
||||
// Reset the cursor of the head block so that it can be reused
|
||||
SkASSERT(block == &fHead);
|
||||
block->fCursor = kDataStart;
|
||||
block->fMetadata = 0;
|
||||
// Unlike in reset(), we don't set the head's next block to null because there are
|
||||
// potentially heap-allocated blocks that are still connected to it.
|
||||
// The released block becomes the new scratch block (if it's bigger), or delete it
|
||||
if (this->scratchBlockSize() < block->fSize) {
|
||||
SkASSERT(block != fHead.fPrev); // sanity check, shouldn't already be the scratch block
|
||||
if (fHead.fPrev) {
|
||||
delete fHead.fPrev;
|
||||
}
|
||||
block->markAsScratch();
|
||||
fHead.fPrev = block;
|
||||
} else {
|
||||
delete block;
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement growth policy (opposite of addBlock()'s increment operations)
|
||||
@ -139,14 +150,15 @@ void GrBlockAllocator::reset() {
|
||||
b->fNext = nullptr;
|
||||
b->fCursor = kDataStart;
|
||||
b->fMetadata = 0;
|
||||
|
||||
// For reset(), but NOT releaseBlock(), the head allocatorMetadata resets too
|
||||
// For reset(), but NOT releaseBlock(), the head allocatorMetadata and scratch block
|
||||
// are reset/destroyed.
|
||||
b->fAllocatorMetadata = 0;
|
||||
this->resetScratchSpace();
|
||||
} else {
|
||||
delete b;
|
||||
}
|
||||
}
|
||||
SkASSERT(fTail == &fHead && fHead.fNext == nullptr &&
|
||||
SkASSERT(fTail == &fHead && fHead.fNext == nullptr && fHead.fPrev == nullptr &&
|
||||
fHead.metadata() == 0 && fHead.fCursor == kDataStart);
|
||||
|
||||
GrowthPolicy gp = static_cast<GrowthPolicy>(fGrowthPolicy);
|
||||
@ -154,6 +166,13 @@ void GrBlockAllocator::reset() {
|
||||
fN1 = 1;
|
||||
}
|
||||
|
||||
void GrBlockAllocator::resetScratchSpace() {
|
||||
if (fHead.fPrev) {
|
||||
delete fHead.fPrev;
|
||||
fHead.fPrev = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void GrBlockAllocator::addBlock(int minimumSize, int maxSize) {
|
||||
SkASSERT(minimumSize > (int) sizeof(Block) && minimumSize <= maxSize);
|
||||
|
||||
@ -161,40 +180,59 @@ void GrBlockAllocator::addBlock(int minimumSize, int maxSize) {
|
||||
static constexpr int kMaxN = (1 << 23) - 1;
|
||||
static_assert(2 * kMaxN <= std::numeric_limits<int32_t>::max()); // Growth policy won't overflow
|
||||
|
||||
// Calculate the 'next' size per growth policy sequence
|
||||
GrowthPolicy gp = static_cast<GrowthPolicy>(fGrowthPolicy);
|
||||
int nextN1 = fN0 + fN1;
|
||||
int nextN0;
|
||||
if (gp == GrowthPolicy::kFixed || gp == GrowthPolicy::kLinear) {
|
||||
nextN0 = fN0;
|
||||
} else if (gp == GrowthPolicy::kFibonacci) {
|
||||
nextN0 = fN1;
|
||||
} else {
|
||||
SkASSERT(gp == GrowthPolicy::kExponential);
|
||||
nextN0 = nextN1;
|
||||
}
|
||||
fN0 = std::min(kMaxN, nextN0);
|
||||
fN1 = std::min(kMaxN, nextN1);
|
||||
|
||||
// However, must guard against overflow here, since all the size-based asserts prevented
|
||||
// alignment/addition overflows, while multiplication requires 2x bits instead of x+1.
|
||||
int sizeIncrement = fBlockIncrement * kAddressAlign;
|
||||
int allocSize;
|
||||
if (maxSize / sizeIncrement < nextN1) {
|
||||
// The growth policy would overflow, so use the max. We've already confirmed that maxSize
|
||||
// will be sufficient for the requested minimumSize
|
||||
allocSize = maxSize;
|
||||
} else {
|
||||
allocSize = std::max(minimumSize, sizeIncrement * nextN1);
|
||||
// Then round to a nice boundary since the block isn't maxing out:
|
||||
auto alignAllocSize = [](int size) {
|
||||
// Round to a nice boundary since the block isn't maxing out:
|
||||
// if allocSize > 32K, aligns on 4K boundary otherwise aligns on max_align_t, to play
|
||||
// nicely with jeMalloc (from SkArenaAlloc).
|
||||
int mask = allocSize > (1 << 15) ? ((1 << 12) - 1) : (kAddressAlign - 1);
|
||||
allocSize = std::min((allocSize + mask) & ~mask, maxSize);
|
||||
int mask = size > (1 << 15) ? ((1 << 12) - 1) : (kAddressAlign - 1);
|
||||
return (size + mask) & ~mask;
|
||||
};
|
||||
|
||||
int allocSize;
|
||||
void* mem = nullptr;
|
||||
if (this->scratchBlockSize() >= minimumSize) {
|
||||
// Activate the scratch block instead of making a new block
|
||||
SkASSERT(fHead.fPrev->isScratch());
|
||||
allocSize = fHead.fPrev->fSize;
|
||||
mem = fHead.fPrev;
|
||||
fHead.fPrev = nullptr;
|
||||
} else if (minimumSize < maxSize) {
|
||||
// Calculate the 'next' size per growth policy sequence
|
||||
GrowthPolicy gp = static_cast<GrowthPolicy>(fGrowthPolicy);
|
||||
int nextN1 = fN0 + fN1;
|
||||
int nextN0;
|
||||
if (gp == GrowthPolicy::kFixed || gp == GrowthPolicy::kLinear) {
|
||||
nextN0 = fN0;
|
||||
} else if (gp == GrowthPolicy::kFibonacci) {
|
||||
nextN0 = fN1;
|
||||
} else {
|
||||
SkASSERT(gp == GrowthPolicy::kExponential);
|
||||
nextN0 = nextN1;
|
||||
}
|
||||
fN0 = std::min(kMaxN, nextN0);
|
||||
fN1 = std::min(kMaxN, nextN1);
|
||||
|
||||
// However, must guard against overflow here, since all the size-based asserts prevented
|
||||
// alignment/addition overflows, while multiplication requires 2x bits instead of x+1.
|
||||
int sizeIncrement = fBlockIncrement * kAddressAlign;
|
||||
if (maxSize / sizeIncrement < nextN1) {
|
||||
// The growth policy would overflow, so use the max. We've already confirmed that
|
||||
// maxSize will be sufficient for the requested minimumSize
|
||||
allocSize = maxSize;
|
||||
} else {
|
||||
allocSize = std::min(alignAllocSize(std::max(minimumSize, sizeIncrement * nextN1)),
|
||||
maxSize);
|
||||
}
|
||||
} else {
|
||||
SkASSERT(minimumSize == maxSize);
|
||||
// Still align on a nice boundary, no max clamping since that would just undo the alignment
|
||||
allocSize = alignAllocSize(minimumSize);
|
||||
}
|
||||
|
||||
// Create new block and append to the linked list of blocks in this allocator
|
||||
void* mem = operator new(allocSize);
|
||||
if (!mem) {
|
||||
mem = operator new(allocSize);
|
||||
}
|
||||
fTail->fNext = new (mem) Block(fTail, allocSize);
|
||||
fTail = fTail->fNext;
|
||||
}
|
||||
@ -207,7 +245,13 @@ void GrBlockAllocator::validate() const {
|
||||
blocks.push_back(block);
|
||||
|
||||
SkASSERT(kAssignedMarker == block->fSentinel);
|
||||
SkASSERT(prev == block->fPrev);
|
||||
if (block == &fHead) {
|
||||
// The head blocks' fPrev may be non-null if it holds a scratch block, but that's not
|
||||
// considered part of the linked list
|
||||
SkASSERT(!prev && (!fHead.fPrev || fHead.fPrev->isScratch()));
|
||||
} else {
|
||||
SkASSERT(prev == block->fPrev);
|
||||
}
|
||||
if (prev) {
|
||||
SkASSERT(prev->fNext == block);
|
||||
}
|
||||
|
@ -132,6 +132,9 @@ public:
|
||||
template <size_t Align, size_t Padding>
|
||||
int alignedOffset(int offset) const;
|
||||
|
||||
bool isScratch() const { return fCursor < 0; }
|
||||
void markAsScratch() { fCursor = -1; }
|
||||
|
||||
SkDEBUGCODE(int fSentinel;) // known value to check for bad back pointers to blocks
|
||||
|
||||
Block* fNext; // doubly-linked list of blocks
|
||||
@ -259,6 +262,33 @@ public:
|
||||
template <size_t Align, size_t Padding = 0>
|
||||
ByteRange allocate(size_t size);
|
||||
|
||||
enum ReserveFlags : unsigned {
|
||||
// If provided to reserve(), the input 'size' will be rounded up to the next size determined
|
||||
// by the growth policy of the GrBlockAllocator. If not, 'size' will be aligned to max_align
|
||||
kIgnoreGrowthPolicy_Flag = 0b01,
|
||||
// If provided to reserve(), the number of available bytes of the current block will not
|
||||
// be used to satisfy the reservation (assuming the contiguous range was long enough to
|
||||
// begin with).
|
||||
kIgnoreExistingBytes_Flag = 0b10,
|
||||
|
||||
kNo_ReserveFlags = 0b00
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure the block allocator has 'size' contiguous available bytes. After calling this
|
||||
* function, currentBlock()->avail<Align, Padding>() may still report less than 'size' if the
|
||||
* reserved space was added as a scratch block. This is done so that anything remaining in
|
||||
* the current block can still be used if a smaller-than-size allocation is requested. If 'size'
|
||||
* is requested by a subsequent allocation, the scratch block will automatically be activated
|
||||
* and the request will not itself trigger any malloc.
|
||||
*
|
||||
* The optional 'flags' controls how the input size is allocated; by default it will attempt
|
||||
* to use available contiguous bytes in the current block and will respect the growth policy
|
||||
* of the allocator.
|
||||
*/
|
||||
template <size_t Align = 1, size_t Padding = 0>
|
||||
void reserve(size_t size, ReserveFlags flags = kNo_ReserveFlags);
|
||||
|
||||
/**
|
||||
* Return a pointer to the start of the current block. This will never be null.
|
||||
*/
|
||||
@ -305,6 +335,10 @@ public:
|
||||
*
|
||||
* If 'block' represents the inline-allocated head block, its cursor and metadata are instead
|
||||
* reset to their defaults.
|
||||
*
|
||||
* If the block is not the head block, it may be kept as a scratch block to be reused for
|
||||
* subsequent allocation requests, instead of making an entirely new block. A scratch block is
|
||||
* not visible when iterating over blocks but is reported in the total size of the allocator.
|
||||
*/
|
||||
void releaseBlock(Block* block);
|
||||
|
||||
@ -314,6 +348,11 @@ public:
|
||||
*/
|
||||
void reset();
|
||||
|
||||
/**
|
||||
* Remove any reserved scratch space, either from calling reserve() or releaseBlock().
|
||||
*/
|
||||
void resetScratchSpace();
|
||||
|
||||
template <bool Forward, bool Const> class BlockIter;
|
||||
|
||||
/**
|
||||
@ -338,6 +377,10 @@ public:
|
||||
void validate() const;
|
||||
#endif
|
||||
|
||||
#if GR_TEST_UTILS
|
||||
int testingOnly_scratchBlockSize() const { return this->scratchBlockSize(); }
|
||||
#endif
|
||||
|
||||
private:
|
||||
static constexpr int kDataStart = sizeof(Block);
|
||||
#ifdef SK_FORCE_8_BYTE_ALIGNMENT
|
||||
@ -369,6 +412,8 @@ private:
|
||||
// that will preserve the static guarantees GrBlockAllocator makes.
|
||||
void addBlock(int minSize, int maxSize);
|
||||
|
||||
int scratchBlockSize() const { return fHead.fPrev ? fHead.fPrev->fSize : 0; }
|
||||
|
||||
Block* fTail; // All non-head blocks are heap allocated; tail will never be null.
|
||||
|
||||
// All remaining state is packed into 64 bits to keep GrBlockAllocator at 16 bytes + head block
|
||||
@ -390,6 +435,9 @@ private:
|
||||
|
||||
// Inline head block, must be at the end so that it can utilize any additional reserved space
|
||||
// from the initial allocation.
|
||||
// The head block's prev pointer may be non-null, which signifies a scratch block that may be
|
||||
// reused instead of allocating an entirely new block (this helps when allocate+release calls
|
||||
// bounce back and forth across the capacity of a block).
|
||||
alignas(kAddressAlign) Block fHead;
|
||||
|
||||
static_assert(kGrowthPolicyCount <= 4);
|
||||
@ -435,6 +483,8 @@ private:
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Template and inline implementations
|
||||
|
||||
GR_MAKE_BITFIELD_OPS(GrBlockAllocator::ReserveFlags)
|
||||
|
||||
template<size_t Align, size_t Padding>
|
||||
constexpr size_t GrBlockAllocator::BlockOverhead() {
|
||||
static_assert(GrAlignTo(kDataStart + Padding, Align) >= sizeof(Block));
|
||||
@ -457,6 +507,29 @@ constexpr size_t GrBlockAllocator::MaxBlockSize() {
|
||||
return BlockOverhead<Align, Padding>() + kMaxAllocationSize;
|
||||
}
|
||||
|
||||
template<size_t Align, size_t Padding>
|
||||
void GrBlockAllocator::reserve(size_t size, ReserveFlags flags) {
|
||||
if (size > kMaxAllocationSize) {
|
||||
SK_ABORT("Allocation too large (%zu bytes requested)", size);
|
||||
}
|
||||
int iSize = (int) size;
|
||||
if ((flags & kIgnoreExistingBytes_Flag) ||
|
||||
this->currentBlock()->avail<Align, Padding>() < iSize) {
|
||||
|
||||
int blockSize = BlockOverhead<Align, Padding>() + iSize;
|
||||
int maxSize = (flags & kIgnoreGrowthPolicy_Flag) ? blockSize
|
||||
: MaxBlockSize<Align, Padding>();
|
||||
SkASSERT((size_t) maxSize <= (MaxBlockSize<Align, Padding>()));
|
||||
|
||||
SkDEBUGCODE(auto oldTail = fTail;)
|
||||
this->addBlock(blockSize, maxSize);
|
||||
SkASSERT(fTail != oldTail);
|
||||
// Releasing the just added block will move it into scratch space, allowing the original
|
||||
// tail's bytes to be used first before the scratch block is activated.
|
||||
this->releaseBlock(fTail);
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t Align, size_t Padding>
|
||||
GrBlockAllocator::ByteRange GrBlockAllocator::allocate(size_t size) {
|
||||
// Amount of extra space for a new block to make sure the allocation can succeed.
|
||||
@ -599,6 +672,12 @@ public:
|
||||
void advance(BlockT* block) {
|
||||
fBlock = block;
|
||||
fNext = block ? (Forward ? block->fNext : block->fPrev) : nullptr;
|
||||
if (!Forward && fNext && fNext->isScratch()) {
|
||||
// For reverse-iteration only, we need to stop at the head, not the scratch block
|
||||
// possibly stashed in head->prev.
|
||||
fNext = nullptr;
|
||||
}
|
||||
SkASSERT(!fNext || !fNext->isScratch());
|
||||
}
|
||||
|
||||
BlockT* fBlock;
|
||||
|
@ -67,7 +67,8 @@ public:
|
||||
bool isEmpty() const {
|
||||
// If size is the same as preallocSize, there aren't any heap blocks, so currentBlock()
|
||||
// is the inline head block.
|
||||
return 0 == this->size() && 0 == fAllocator.currentBlock()->metadata();
|
||||
return fAllocator.currentBlock() == fAllocator.headBlock() &&
|
||||
fAllocator.currentBlock()->metadata() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,6 +65,19 @@ public:
|
||||
return *new (this->pushItem()) T(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate, if needed, space to hold N more Ts before another malloc will occur.
|
||||
*/
|
||||
void reserve(int n) {
|
||||
int avail = fAllocator->currentBlock()->template avail<alignof(T)>() / sizeof(T);
|
||||
if (n > avail) {
|
||||
int reserved = n - avail;
|
||||
// Don't consider existing bytes since we've already determined how to split the N items
|
||||
fAllocator->template reserve<alignof(T)>(
|
||||
reserved * sizeof(T), GrBlockAllocator::kIgnoreExistingBytes_Flag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the last item, only call if count() != 0
|
||||
*/
|
||||
@ -156,7 +169,7 @@ public:
|
||||
* Use for-range loops by calling items() or ritems() instead to access all added items in order
|
||||
*/
|
||||
T& item(int i) {
|
||||
SkASSERT(i >= 0 && i < fAllocator->metadata());
|
||||
SkASSERT(i >= 0 && i < this->count());
|
||||
|
||||
// Iterate over blocks until we find the one that contains i.
|
||||
for (auto* b : fAllocator->blocks()) {
|
||||
@ -236,6 +249,11 @@ public:
|
||||
// Iterate from newest to oldest using a for-range loop.
|
||||
RIter ritems() { return RIter(fAllocator.allocator()); }
|
||||
CRIter ritems() const { return CRIter(fAllocator.allocator()); }
|
||||
|
||||
#if GR_TEST_UTILS
|
||||
// For introspection
|
||||
const GrBlockAllocator* allocator() const { return fAllocator.allocator(); }
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -38,13 +38,22 @@ static Block* get_block(GrSBlockAllocator<N>& pool, int blockIndex) {
|
||||
return found;
|
||||
}
|
||||
|
||||
// GrBlockAllocator holds on to the largest last-released block to reuse for new allocations,
|
||||
// and this is still counted in its totalSize(). However, it's easier to reason about size - scratch
|
||||
// in many of these tests.
|
||||
template<size_t N>
|
||||
static size_t total_size(GrSBlockAllocator<N>& pool) {
|
||||
return pool->totalSize() - pool->testingOnly_scratchBlockSize();
|
||||
}
|
||||
|
||||
template<size_t N>
|
||||
static size_t add_block(GrSBlockAllocator<N>& pool) {
|
||||
size_t currentSize = pool->totalSize();
|
||||
while(pool->totalSize() == currentSize) {
|
||||
size_t currentSize = total_size(pool);
|
||||
GrBlockAllocator::Block* current = pool->currentBlock();
|
||||
while(pool->currentBlock() == current) {
|
||||
pool->template allocate<4>(pool->preallocSize() / 2);
|
||||
}
|
||||
return pool->totalSize() - currentSize;
|
||||
return total_size(pool) - currentSize;
|
||||
}
|
||||
|
||||
template<size_t N>
|
||||
@ -124,7 +133,7 @@ DEF_TEST(GrBlockAllocatorAlloc, r) {
|
||||
validate_ptr(32, 96, p32, &p16);
|
||||
|
||||
// All of these allocations should be in the head block
|
||||
REPORTER_ASSERT(r, pool->totalSize() == pool->preallocSize());
|
||||
REPORTER_ASSERT(r, total_size(pool) == pool->preallocSize());
|
||||
SkDEBUGCODE(pool->validate();)
|
||||
|
||||
// Requesting an allocation of avail() should not make a new block
|
||||
@ -137,21 +146,21 @@ DEF_TEST(GrBlockAllocatorAlloc, r) {
|
||||
REPORTER_ASSERT(r, pool->currentBlock()->avail<4>() < 4);
|
||||
auto pNextBlock = pool->allocate<4>(4);
|
||||
validate_ptr(4, 4, pNextBlock, nullptr);
|
||||
REPORTER_ASSERT(r, pool->totalSize() > pool->preallocSize());
|
||||
REPORTER_ASSERT(r, total_size(pool) > pool->preallocSize());
|
||||
|
||||
// Allocating more than avail() makes an another block
|
||||
size_t currentSize = pool->totalSize();
|
||||
size_t currentSize = total_size(pool);
|
||||
size_t bigRequest = pool->currentBlock()->avail<4>() * 2;
|
||||
auto pTooBig = pool->allocate<4>(bigRequest);
|
||||
validate_ptr(4, bigRequest, pTooBig, nullptr);
|
||||
REPORTER_ASSERT(r, pool->totalSize() > currentSize);
|
||||
REPORTER_ASSERT(r, total_size(pool) > currentSize);
|
||||
|
||||
// Allocating more than the default growth policy (1024 in this case), will fulfill the request
|
||||
REPORTER_ASSERT(r, pool->totalSize() - currentSize < 4096);
|
||||
currentSize = pool->totalSize();
|
||||
REPORTER_ASSERT(r, total_size(pool) - currentSize < 4096);
|
||||
currentSize = total_size(pool);
|
||||
auto pReallyTooBig = pool->allocate<4>(4096);
|
||||
validate_ptr(4, 4096, pReallyTooBig, nullptr);
|
||||
REPORTER_ASSERT(r, pool->totalSize() >= currentSize + 4096);
|
||||
REPORTER_ASSERT(r, total_size(pool) >= currentSize + 4096);
|
||||
SkDEBUGCODE(pool->validate();)
|
||||
}
|
||||
|
||||
@ -276,7 +285,7 @@ DEF_TEST(GrBlockAllocatorGrowthPolicy, r) {
|
||||
GrSBlockAllocator<kInitSize> pool{(GrowthPolicy) gp};
|
||||
SkDEBUGCODE(pool->validate();)
|
||||
|
||||
REPORTER_ASSERT(r, kExpectedSizes[gp][0] == pool->totalSize());
|
||||
REPORTER_ASSERT(r, kExpectedSizes[gp][0] == total_size(pool));
|
||||
for (int i = 1; i < kBlockCount; ++i) {
|
||||
REPORTER_ASSERT(r, kExpectedSizes[gp][i] == add_block(pool));
|
||||
}
|
||||
@ -327,7 +336,7 @@ DEF_TEST(GrBlockAllocatorReleaseBlock, r) {
|
||||
|
||||
void* firstAlloc = alloc_byte(pool);
|
||||
|
||||
size_t b1Size = pool->totalSize();
|
||||
size_t b1Size = total_size(pool);
|
||||
size_t b2Size = add_block(pool);
|
||||
size_t b3Size = add_block(pool);
|
||||
size_t b4Size = add_block(pool);
|
||||
@ -340,30 +349,31 @@ DEF_TEST(GrBlockAllocatorReleaseBlock, r) {
|
||||
|
||||
// Remove the 3 added blocks, but always remove the i = 1 to test intermediate removal (and
|
||||
// on the last iteration, will test tail removal).
|
||||
REPORTER_ASSERT(r, pool->totalSize() == b1Size + b2Size + b3Size + b4Size);
|
||||
REPORTER_ASSERT(r, total_size(pool) == b1Size + b2Size + b3Size + b4Size);
|
||||
pool->releaseBlock(get_block(pool, 1));
|
||||
REPORTER_ASSERT(r, block_count(pool) == 3);
|
||||
REPORTER_ASSERT(r, get_block(pool, 1)->metadata() == 3);
|
||||
REPORTER_ASSERT(r, pool->totalSize() == b1Size + b3Size + b4Size);
|
||||
REPORTER_ASSERT(r, total_size(pool) == b1Size + b3Size + b4Size);
|
||||
|
||||
pool->releaseBlock(get_block(pool, 1));
|
||||
REPORTER_ASSERT(r, block_count(pool) == 2);
|
||||
REPORTER_ASSERT(r, get_block(pool, 1)->metadata() == 4);
|
||||
REPORTER_ASSERT(r, pool->totalSize() == b1Size + b4Size);
|
||||
REPORTER_ASSERT(r, total_size(pool) == b1Size + b4Size);
|
||||
|
||||
pool->releaseBlock(get_block(pool, 1));
|
||||
REPORTER_ASSERT(r, block_count(pool) == 1);
|
||||
REPORTER_ASSERT(r, pool->totalSize() == b1Size);
|
||||
REPORTER_ASSERT(r, total_size(pool) == b1Size);
|
||||
|
||||
// Since we're back to just the head block, if we add a new block, the growth policy should
|
||||
// match the original sequence instead of continuing with "b5Size'"
|
||||
pool->resetScratchSpace();
|
||||
size_t size = add_block(pool);
|
||||
REPORTER_ASSERT(r, size == b2Size);
|
||||
pool->releaseBlock(get_block(pool, 1));
|
||||
|
||||
// Explicitly release the head block and confirm it's reset
|
||||
pool->releaseBlock(get_block(pool, 0));
|
||||
REPORTER_ASSERT(r, pool->totalSize() == pool->preallocSize());
|
||||
REPORTER_ASSERT(r, total_size(pool) == pool->preallocSize());
|
||||
REPORTER_ASSERT(r, block_count(pool) == 1);
|
||||
REPORTER_ASSERT(r, firstAlloc == alloc_byte(pool));
|
||||
REPORTER_ASSERT(r, get_block(pool, 0)->metadata() == 0); // metadata reset too
|
||||
@ -415,6 +425,69 @@ DEF_TEST(GrBlockAllocatorIterateAndRelease, r) {
|
||||
REPORTER_ASSERT(r, block_count(pool) == 1);
|
||||
}
|
||||
|
||||
DEF_TEST(GrBlockAllocatorScratchBlockReserve, r) {
|
||||
GrSBlockAllocator<256> pool;
|
||||
|
||||
size_t added = add_block(pool);
|
||||
REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() == 0);
|
||||
size_t total = pool->totalSize();
|
||||
pool->releaseBlock(pool->currentBlock());
|
||||
|
||||
// Total size shouldn't have changed, the released block should become scratch
|
||||
REPORTER_ASSERT(r, pool->totalSize() == total);
|
||||
REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() == added);
|
||||
|
||||
// But a reset definitely deletes any scratch block
|
||||
pool->reset();
|
||||
REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() == 0);
|
||||
|
||||
// Reserving more than what's available adds a scratch block, and current block remains avail.
|
||||
size_t avail = pool->currentBlock()->avail();
|
||||
size_t reserve = avail + 1;
|
||||
pool->reserve(reserve);
|
||||
REPORTER_ASSERT(r, (size_t) pool->currentBlock()->avail() == avail);
|
||||
// And rounds up to the fixed size of this pool's growth policy
|
||||
REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() >= reserve &&
|
||||
pool->testingOnly_scratchBlockSize() % 256 == 0);
|
||||
|
||||
// Allocating more than avail activates the scratch block (so totalSize doesn't change)
|
||||
size_t preAllocTotalSize = pool->totalSize();
|
||||
pool->allocate<1>(avail + 1);
|
||||
REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() == 0);
|
||||
REPORTER_ASSERT(r, pool->totalSize() == preAllocTotalSize);
|
||||
|
||||
// When reserving less than what's still available in the current block, no scratch block is
|
||||
// added.
|
||||
pool->reserve(pool->currentBlock()->avail());
|
||||
REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() == 0);
|
||||
|
||||
// Unless checking available bytes is disabled
|
||||
pool->reserve(pool->currentBlock()->avail(), GrBlockAllocator::kIgnoreExistingBytes_Flag);
|
||||
REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() > 0);
|
||||
|
||||
// If kIgnoreGrowthPolicy is specified, the new scratch block should not have been updated to
|
||||
// follow the size (which in this case is a fixed 256 bytes per block).
|
||||
pool->resetScratchSpace();
|
||||
pool->reserve(32, GrBlockAllocator::kIgnoreGrowthPolicy_Flag);
|
||||
REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() > 0 &&
|
||||
pool->testingOnly_scratchBlockSize() < 256);
|
||||
|
||||
// When requesting an allocation larger than the current block and the scratch block, a new
|
||||
// block is added, and the scratch block remains scratch.
|
||||
GrBlockAllocator::Block* oldTail = pool->currentBlock();
|
||||
avail = oldTail->avail();
|
||||
size_t scratchAvail = 2 * avail;
|
||||
pool->reserve(scratchAvail);
|
||||
REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() >= scratchAvail); // sanity
|
||||
|
||||
// This allocation request is higher than oldTail's available, and the scratch size so we
|
||||
// should add a new block and scratch size should stay the same.
|
||||
scratchAvail = pool->testingOnly_scratchBlockSize();
|
||||
pool->allocate<1>(scratchAvail + 1);
|
||||
REPORTER_ASSERT(r, pool->currentBlock() != oldTail);
|
||||
REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() == scratchAvail);
|
||||
}
|
||||
|
||||
// These tests ensure that the allocation padding mechanism works as intended
|
||||
struct TestMeta {
|
||||
int fX1;
|
||||
|
@ -22,9 +22,13 @@ struct C {
|
||||
|
||||
int fID;
|
||||
|
||||
// Under the hood, GrTAllocator and GrBlockAllocator round up to max_align_t. If 'C' was just
|
||||
// 4 bytes, that often means the internal blocks can squeeze a few extra instances in. This
|
||||
// is fine, but makes predicting a little trickier, so make sure C is a bit bigger.
|
||||
int fPadding[4];
|
||||
|
||||
static int gInstCnt;
|
||||
};
|
||||
|
||||
int C::gInstCnt = 0;
|
||||
}
|
||||
|
||||
@ -156,6 +160,63 @@ static void run_allocator_test(GrTAllocator<C, N>* allocator, skiatest::Reporter
|
||||
check_allocator(allocator, 100, 10, reporter);
|
||||
}
|
||||
|
||||
|
||||
template<int N>
|
||||
static void run_reserve_test(skiatest::Reporter* reporter) {
|
||||
constexpr int kItemsPerBlock = N + 4; // Make this a number > 1, even if N starting items == 1
|
||||
|
||||
GrTAllocator<C, N> list(kItemsPerBlock);
|
||||
size_t initialSize = list.allocator()->totalSize();
|
||||
// Should be able to add N instances of T w/o changing size from initialSize
|
||||
for (int i = 0; i < N; ++i) {
|
||||
list.push_back(C(i));
|
||||
}
|
||||
REPORTER_ASSERT(reporter, initialSize == list.allocator()->totalSize());
|
||||
|
||||
// Reserve room for 2*kItemsPerBlock items
|
||||
list.reserve(2 * kItemsPerBlock);
|
||||
REPORTER_ASSERT(reporter, list.count() == N); // count shouldn't change though
|
||||
|
||||
size_t reservedSize = list.allocator()->totalSize();
|
||||
REPORTER_ASSERT(reporter, reservedSize >= initialSize + 2 * kItemsPerBlock * sizeof(C));
|
||||
for (int i = 0; i < 2 * kItemsPerBlock; ++i) {
|
||||
list.push_back(C(i));
|
||||
}
|
||||
REPORTER_ASSERT(reporter, reservedSize == list.allocator()->totalSize());
|
||||
|
||||
// Make the next block partially fully (N > 0 but < kItemsPerBlock)
|
||||
for (int i = 0; i < N; ++i) {
|
||||
list.push_back(C(i));
|
||||
}
|
||||
|
||||
// Reserve room again for 2*kItemsPerBlock, but reserve should automatically take account of the
|
||||
// (kItemsPerBlock-N) that are still available in the active block
|
||||
list.reserve(2 * kItemsPerBlock);
|
||||
int extraReservedCount = kItemsPerBlock + N;
|
||||
// Because GrTAllocator normally allocates blocks in fixed sizes, and extraReservedCount >
|
||||
// items-per-block, it will always use that size and not that of the growth policy.
|
||||
REPORTER_ASSERT(reporter, (size_t) list.allocator()->testingOnly_scratchBlockSize() >=
|
||||
extraReservedCount * sizeof(C));
|
||||
|
||||
reservedSize = list.allocator()->totalSize();
|
||||
for (int i = 0; i < 2 * kItemsPerBlock; ++i) {
|
||||
list.push_back(C(i));
|
||||
}
|
||||
REPORTER_ASSERT(reporter, reservedSize == list.allocator()->totalSize());
|
||||
|
||||
// If we reserve a count < items-per-block, it will use the fixed size from the growth policy.
|
||||
list.reserve(2);
|
||||
REPORTER_ASSERT(reporter, (size_t) list.allocator()->testingOnly_scratchBlockSize() >=
|
||||
kItemsPerBlock * sizeof(C));
|
||||
|
||||
// Ensure the reservations didn't initialize any more D's than anticipated
|
||||
int expectedInstanceCount = 2 * (N + 2 * kItemsPerBlock);
|
||||
REPORTER_ASSERT(reporter, expectedInstanceCount == C::gInstCnt);
|
||||
|
||||
list.reset();
|
||||
REPORTER_ASSERT(reporter, 0 == C::gInstCnt);
|
||||
}
|
||||
|
||||
DEF_TEST(GrTAllocator, reporter) {
|
||||
// Test combinations of allocators with and without stack storage and with different block sizes
|
||||
GrTAllocator<C> a1(1);
|
||||
@ -175,4 +236,10 @@ DEF_TEST(GrTAllocator, reporter) {
|
||||
|
||||
GrTAllocator<C, 4> sa4;
|
||||
run_allocator_test(&sa4, reporter);
|
||||
|
||||
run_reserve_test<1>(reporter);
|
||||
run_reserve_test<2>(reporter);
|
||||
run_reserve_test<3>(reporter);
|
||||
run_reserve_test<4>(reporter);
|
||||
run_reserve_test<5>(reporter);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user