Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 68e5f29

Browse files
lhkbobSkia Commit-Bot
authored andcommitted
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 <[email protected]> Reviewed-by: Robert Phillips <[email protected]> Commit-Queue: Michael Ludwig <[email protected]>
1 parent dc945ea commit 68e5f29

File tree

6 files changed

+347
-65
lines changed

6 files changed

+347
-65
lines changed

src/gpu/GrBlockAllocator.cpp

Lines changed: 89 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ GrBlockAllocator::Block::~Block() {
4646

4747
size_t GrBlockAllocator::totalSize() const {
4848
// Use size_t since the sum across all blocks could exceed 'int', even though each block won't
49-
size_t size = offsetof(GrBlockAllocator, fHead);
49+
size_t size = offsetof(GrBlockAllocator, fHead) + this->scratchBlockSize();
5050
for (const Block* b : this->blocks()) {
5151
size += b->fSize;
5252
}
@@ -55,7 +55,10 @@ size_t GrBlockAllocator::totalSize() const {
5555
}
5656

5757
size_t GrBlockAllocator::totalUsableSpace() const {
58-
size_t size = 0;
58+
size_t size = this->scratchBlockSize();
59+
if (size > 0) {
60+
size -= kDataStart; // scratchBlockSize reports total block size, not usable size
61+
}
5962
for (const Block* b : this->blocks()) {
6063
size += (b->fSize - kDataStart);
6164
}
@@ -87,9 +90,14 @@ GrBlockAllocator::Block* GrBlockAllocator::findOwningBlock(const void* p) {
8790
}
8891

8992
void GrBlockAllocator::releaseBlock(Block* block) {
90-
if (block->fPrev) {
91-
// Unlink block from the double-linked list of blocks
92-
SkASSERT(block != &fHead);
93+
if (block == &fHead) {
94+
// Reset the cursor of the head block so that it can be reused if it becomes the new tail
95+
block->fCursor = kDataStart;
96+
block->fMetadata = 0;
97+
// Unlike in reset(), we don't set the head's next block to null because there are
98+
// potentially heap-allocated blocks that are still connected to it.
99+
} else {
100+
SkASSERT(block->fPrev);
93101
block->fPrev->fNext = block->fNext;
94102
if (block->fNext) {
95103
SkASSERT(fTail != block);
@@ -99,14 +107,17 @@ void GrBlockAllocator::releaseBlock(Block* block) {
99107
fTail = block->fPrev;
100108
}
101109

102-
delete block;
103-
} else {
104-
// Reset the cursor of the head block so that it can be reused
105-
SkASSERT(block == &fHead);
106-
block->fCursor = kDataStart;
107-
block->fMetadata = 0;
108-
// Unlike in reset(), we don't set the head's next block to null because there are
109-
// potentially heap-allocated blocks that are still connected to it.
110+
// The released block becomes the new scratch block (if it's bigger), or delete it
111+
if (this->scratchBlockSize() < block->fSize) {
112+
SkASSERT(block != fHead.fPrev); // sanity check, shouldn't already be the scratch block
113+
if (fHead.fPrev) {
114+
delete fHead.fPrev;
115+
}
116+
block->markAsScratch();
117+
fHead.fPrev = block;
118+
} else {
119+
delete block;
120+
}
110121
}
111122

112123
// Decrement growth policy (opposite of addBlock()'s increment operations)
@@ -139,62 +150,89 @@ void GrBlockAllocator::reset() {
139150
b->fNext = nullptr;
140151
b->fCursor = kDataStart;
141152
b->fMetadata = 0;
142-
143-
// For reset(), but NOT releaseBlock(), the head allocatorMetadata resets too
153+
// For reset(), but NOT releaseBlock(), the head allocatorMetadata and scratch block
154+
// are reset/destroyed.
144155
b->fAllocatorMetadata = 0;
156+
this->resetScratchSpace();
145157
} else {
146158
delete b;
147159
}
148160
}
149-
SkASSERT(fTail == &fHead && fHead.fNext == nullptr &&
161+
SkASSERT(fTail == &fHead && fHead.fNext == nullptr && fHead.fPrev == nullptr &&
150162
fHead.metadata() == 0 && fHead.fCursor == kDataStart);
151163

152164
GrowthPolicy gp = static_cast<GrowthPolicy>(fGrowthPolicy);
153165
fN0 = (gp == GrowthPolicy::kLinear || gp == GrowthPolicy::kExponential) ? 1 : 0;
154166
fN1 = 1;
155167
}
156168

169+
void GrBlockAllocator::resetScratchSpace() {
170+
if (fHead.fPrev) {
171+
delete fHead.fPrev;
172+
fHead.fPrev = nullptr;
173+
}
174+
}
175+
157176
void GrBlockAllocator::addBlock(int minimumSize, int maxSize) {
158177
SkASSERT(minimumSize > (int) sizeof(Block) && minimumSize <= maxSize);
159178

160179
// Max positive value for uint:23 storage (decltype(fN0) picks up uint64_t, not uint:23).
161180
static constexpr int kMaxN = (1 << 23) - 1;
162181
static_assert(2 * kMaxN <= std::numeric_limits<int32_t>::max()); // Growth policy won't overflow
163182

164-
// Calculate the 'next' size per growth policy sequence
165-
GrowthPolicy gp = static_cast<GrowthPolicy>(fGrowthPolicy);
166-
int nextN1 = fN0 + fN1;
167-
int nextN0;
168-
if (gp == GrowthPolicy::kFixed || gp == GrowthPolicy::kLinear) {
169-
nextN0 = fN0;
170-
} else if (gp == GrowthPolicy::kFibonacci) {
171-
nextN0 = fN1;
172-
} else {
173-
SkASSERT(gp == GrowthPolicy::kExponential);
174-
nextN0 = nextN1;
175-
}
176-
fN0 = std::min(kMaxN, nextN0);
177-
fN1 = std::min(kMaxN, nextN1);
183+
auto alignAllocSize = [](int size) {
184+
// Round to a nice boundary since the block isn't maxing out:
185+
// if allocSize > 32K, aligns on 4K boundary otherwise aligns on max_align_t, to play
186+
// nicely with jeMalloc (from SkArenaAlloc).
187+
int mask = size > (1 << 15) ? ((1 << 12) - 1) : (kAddressAlign - 1);
188+
return (size + mask) & ~mask;
189+
};
178190

179-
// However, must guard against overflow here, since all the size-based asserts prevented
180-
// alignment/addition overflows, while multiplication requires 2x bits instead of x+1.
181-
int sizeIncrement = fBlockIncrement * kAddressAlign;
182191
int allocSize;
183-
if (maxSize / sizeIncrement < nextN1) {
184-
// The growth policy would overflow, so use the max. We've already confirmed that maxSize
185-
// will be sufficient for the requested minimumSize
186-
allocSize = maxSize;
192+
void* mem = nullptr;
193+
if (this->scratchBlockSize() >= minimumSize) {
194+
// Activate the scratch block instead of making a new block
195+
SkASSERT(fHead.fPrev->isScratch());
196+
allocSize = fHead.fPrev->fSize;
197+
mem = fHead.fPrev;
198+
fHead.fPrev = nullptr;
199+
} else if (minimumSize < maxSize) {
200+
// Calculate the 'next' size per growth policy sequence
201+
GrowthPolicy gp = static_cast<GrowthPolicy>(fGrowthPolicy);
202+
int nextN1 = fN0 + fN1;
203+
int nextN0;
204+
if (gp == GrowthPolicy::kFixed || gp == GrowthPolicy::kLinear) {
205+
nextN0 = fN0;
206+
} else if (gp == GrowthPolicy::kFibonacci) {
207+
nextN0 = fN1;
208+
} else {
209+
SkASSERT(gp == GrowthPolicy::kExponential);
210+
nextN0 = nextN1;
211+
}
212+
fN0 = std::min(kMaxN, nextN0);
213+
fN1 = std::min(kMaxN, nextN1);
214+
215+
// However, must guard against overflow here, since all the size-based asserts prevented
216+
// alignment/addition overflows, while multiplication requires 2x bits instead of x+1.
217+
int sizeIncrement = fBlockIncrement * kAddressAlign;
218+
if (maxSize / sizeIncrement < nextN1) {
219+
// The growth policy would overflow, so use the max. We've already confirmed that
220+
// maxSize will be sufficient for the requested minimumSize
221+
allocSize = maxSize;
222+
} else {
223+
allocSize = std::min(alignAllocSize(std::max(minimumSize, sizeIncrement * nextN1)),
224+
maxSize);
225+
}
187226
} else {
188-
allocSize = std::max(minimumSize, sizeIncrement * nextN1);
189-
// Then round to a nice boundary since the block isn't maxing out:
190-
// if allocSize > 32K, aligns on 4K boundary otherwise aligns on max_align_t, to play
191-
// nicely with jeMalloc (from SkArenaAlloc).
192-
int mask = allocSize > (1 << 15) ? ((1 << 12) - 1) : (kAddressAlign - 1);
193-
allocSize = std::min((allocSize + mask) & ~mask, maxSize);
227+
SkASSERT(minimumSize == maxSize);
228+
// Still align on a nice boundary, no max clamping since that would just undo the alignment
229+
allocSize = alignAllocSize(minimumSize);
194230
}
195231

196232
// Create new block and append to the linked list of blocks in this allocator
197-
void* mem = operator new(allocSize);
233+
if (!mem) {
234+
mem = operator new(allocSize);
235+
}
198236
fTail->fNext = new (mem) Block(fTail, allocSize);
199237
fTail = fTail->fNext;
200238
}
@@ -207,7 +245,13 @@ void GrBlockAllocator::validate() const {
207245
blocks.push_back(block);
208246

209247
SkASSERT(kAssignedMarker == block->fSentinel);
210-
SkASSERT(prev == block->fPrev);
248+
if (block == &fHead) {
249+
// The head blocks' fPrev may be non-null if it holds a scratch block, but that's not
250+
// considered part of the linked list
251+
SkASSERT(!prev && (!fHead.fPrev || fHead.fPrev->isScratch()));
252+
} else {
253+
SkASSERT(prev == block->fPrev);
254+
}
211255
if (prev) {
212256
SkASSERT(prev->fNext == block);
213257
}

src/gpu/GrBlockAllocator.h

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ class GrBlockAllocator final : SkNoncopyable {
132132
template <size_t Align, size_t Padding>
133133
int alignedOffset(int offset) const;
134134

135+
bool isScratch() const { return fCursor < 0; }
136+
void markAsScratch() { fCursor = -1; }
137+
135138
SkDEBUGCODE(int fSentinel;) // known value to check for bad back pointers to blocks
136139

137140
Block* fNext; // doubly-linked list of blocks
@@ -259,6 +262,33 @@ class GrBlockAllocator final : SkNoncopyable {
259262
template <size_t Align, size_t Padding = 0>
260263
ByteRange allocate(size_t size);
261264

265+
enum ReserveFlags : unsigned {
266+
// If provided to reserve(), the input 'size' will be rounded up to the next size determined
267+
// by the growth policy of the GrBlockAllocator. If not, 'size' will be aligned to max_align
268+
kIgnoreGrowthPolicy_Flag = 0b01,
269+
// If provided to reserve(), the number of available bytes of the current block will not
270+
// be used to satisfy the reservation (assuming the contiguous range was long enough to
271+
// begin with).
272+
kIgnoreExistingBytes_Flag = 0b10,
273+
274+
kNo_ReserveFlags = 0b00
275+
};
276+
277+
/**
278+
* Ensure the block allocator has 'size' contiguous available bytes. After calling this
279+
* function, currentBlock()->avail<Align, Padding>() may still report less than 'size' if the
280+
* reserved space was added as a scratch block. This is done so that anything remaining in
281+
* the current block can still be used if a smaller-than-size allocation is requested. If 'size'
282+
* is requested by a subsequent allocation, the scratch block will automatically be activated
283+
* and the request will not itself trigger any malloc.
284+
*
285+
* The optional 'flags' controls how the input size is allocated; by default it will attempt
286+
* to use available contiguous bytes in the current block and will respect the growth policy
287+
* of the allocator.
288+
*/
289+
template <size_t Align = 1, size_t Padding = 0>
290+
void reserve(size_t size, ReserveFlags flags = kNo_ReserveFlags);
291+
262292
/**
263293
* Return a pointer to the start of the current block. This will never be null.
264294
*/
@@ -305,6 +335,10 @@ class GrBlockAllocator final : SkNoncopyable {
305335
*
306336
* If 'block' represents the inline-allocated head block, its cursor and metadata are instead
307337
* reset to their defaults.
338+
*
339+
* If the block is not the head block, it may be kept as a scratch block to be reused for
340+
* subsequent allocation requests, instead of making an entirely new block. A scratch block is
341+
* not visible when iterating over blocks but is reported in the total size of the allocator.
308342
*/
309343
void releaseBlock(Block* block);
310344

@@ -314,6 +348,11 @@ class GrBlockAllocator final : SkNoncopyable {
314348
*/
315349
void reset();
316350

351+
/**
352+
* Remove any reserved scratch space, either from calling reserve() or releaseBlock().
353+
*/
354+
void resetScratchSpace();
355+
317356
template <bool Forward, bool Const> class BlockIter;
318357

319358
/**
@@ -338,6 +377,10 @@ class GrBlockAllocator final : SkNoncopyable {
338377
void validate() const;
339378
#endif
340379

380+
#if GR_TEST_UTILS
381+
int testingOnly_scratchBlockSize() const { return this->scratchBlockSize(); }
382+
#endif
383+
341384
private:
342385
static constexpr int kDataStart = sizeof(Block);
343386
#ifdef SK_FORCE_8_BYTE_ALIGNMENT
@@ -369,6 +412,8 @@ class GrBlockAllocator final : SkNoncopyable {
369412
// that will preserve the static guarantees GrBlockAllocator makes.
370413
void addBlock(int minSize, int maxSize);
371414

415+
int scratchBlockSize() const { return fHead.fPrev ? fHead.fPrev->fSize : 0; }
416+
372417
Block* fTail; // All non-head blocks are heap allocated; tail will never be null.
373418

374419
// All remaining state is packed into 64 bits to keep GrBlockAllocator at 16 bytes + head block
@@ -390,6 +435,9 @@ class GrBlockAllocator final : SkNoncopyable {
390435

391436
// Inline head block, must be at the end so that it can utilize any additional reserved space
392437
// from the initial allocation.
438+
// The head block's prev pointer may be non-null, which signifies a scratch block that may be
439+
// reused instead of allocating an entirely new block (this helps when allocate+release calls
440+
// bounce back and forth across the capacity of a block).
393441
alignas(kAddressAlign) Block fHead;
394442

395443
static_assert(kGrowthPolicyCount <= 4);
@@ -435,6 +483,8 @@ class GrSBlockAllocator : SkNoncopyable {
435483
///////////////////////////////////////////////////////////////////////////////////////////////////
436484
// Template and inline implementations
437485

486+
GR_MAKE_BITFIELD_OPS(GrBlockAllocator::ReserveFlags)
487+
438488
template<size_t Align, size_t Padding>
439489
constexpr size_t GrBlockAllocator::BlockOverhead() {
440490
static_assert(GrAlignTo(kDataStart + Padding, Align) >= sizeof(Block));
@@ -457,6 +507,29 @@ constexpr size_t GrBlockAllocator::MaxBlockSize() {
457507
return BlockOverhead<Align, Padding>() + kMaxAllocationSize;
458508
}
459509

510+
template<size_t Align, size_t Padding>
511+
void GrBlockAllocator::reserve(size_t size, ReserveFlags flags) {
512+
if (size > kMaxAllocationSize) {
513+
SK_ABORT("Allocation too large (%zu bytes requested)", size);
514+
}
515+
int iSize = (int) size;
516+
if ((flags & kIgnoreExistingBytes_Flag) ||
517+
this->currentBlock()->avail<Align, Padding>() < iSize) {
518+
519+
int blockSize = BlockOverhead<Align, Padding>() + iSize;
520+
int maxSize = (flags & kIgnoreGrowthPolicy_Flag) ? blockSize
521+
: MaxBlockSize<Align, Padding>();
522+
SkASSERT((size_t) maxSize <= (MaxBlockSize<Align, Padding>()));
523+
524+
SkDEBUGCODE(auto oldTail = fTail;)
525+
this->addBlock(blockSize, maxSize);
526+
SkASSERT(fTail != oldTail);
527+
// Releasing the just added block will move it into scratch space, allowing the original
528+
// tail's bytes to be used first before the scratch block is activated.
529+
this->releaseBlock(fTail);
530+
}
531+
}
532+
460533
template <size_t Align, size_t Padding>
461534
GrBlockAllocator::ByteRange GrBlockAllocator::allocate(size_t size) {
462535
// Amount of extra space for a new block to make sure the allocation can succeed.
@@ -599,6 +672,12 @@ class GrBlockAllocator::BlockIter {
599672
void advance(BlockT* block) {
600673
fBlock = block;
601674
fNext = block ? (Forward ? block->fNext : block->fPrev) : nullptr;
675+
if (!Forward && fNext && fNext->isScratch()) {
676+
// For reverse-iteration only, we need to stop at the head, not the scratch block
677+
// possibly stashed in head->prev.
678+
fNext = nullptr;
679+
}
680+
SkASSERT(!fNext || !fNext->isScratch());
602681
}
603682

604683
BlockT* fBlock;

src/gpu/GrMemoryPool.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ class GrMemoryPool {
6767
bool isEmpty() const {
6868
// If size is the same as preallocSize, there aren't any heap blocks, so currentBlock()
6969
// is the inline head block.
70-
return 0 == this->size() && 0 == fAllocator.currentBlock()->metadata();
70+
return fAllocator.currentBlock() == fAllocator.headBlock() &&
71+
fAllocator.currentBlock()->metadata() == 0;
7172
}
7273

7374
/**

0 commit comments

Comments
 (0)