/* * Copyright 2010 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #ifndef SkTBlockList_DEFINED #define SkTBlockList_DEFINED #include "include/private/base/SkAssert.h" #include "include/private/base/SkDebug.h" #include "include/private/base/SkTo.h" #include "src/base/SkBlockAllocator.h" #include #include #include #include // Forward declarations for the iterators used by SkTBlockList using IndexFn = int (*)(const SkBlockAllocator::Block*); using NextFn = int (*)(const SkBlockAllocator::Block*, int); template using ItemFn = T (*)(B*, int); template ::type> Resolve> class BlockIndexIterator; /** * SkTBlockList manages dynamic storage for instances of T, reserving fixed blocks such that * allocation is amortized across every N instances. In this way it is a hybrid of an array-based * vector and a linked-list. T can be any type and non-trivial destructors are automatically * invoked when the SkTBlockList is destructed. The addresses of instances are guaranteed * not to move except when a list is concatenated to another. * * The collection supports storing a templated number of elements inline before heap-allocated * blocks are made to hold additional instances. By default, the heap blocks are sized to hold the * same number of items as the inline block. A common pattern is to have the inline size hold only * a small number of items for the common case and then allocate larger blocks when needed. * * If the size of a collection is N, and its block size is B, the complexity of the common * operations are: * - push_back()/emplace_back(): O(1), with malloc O(B) * - pop_back(): O(1), with free O(B) * - front()/back(): O(1) * - reset(): O(N) for non-trivial types, O(N/B) for trivial types * - concat(): O(B) * - random access: O(N/B) * - iteration: O(1) at each step * * These characteristics make it well suited for allocating items in a LIFO ordering, or otherwise * acting as a stack, or simply using it as a typed allocator. */ template class SkTBlockList { public: /** * Create an list that defaults to using StartingItems as heap increment and the * kFixed growth policy (e.g. all allocations will match StartingItems). */ SkTBlockList() : SkTBlockList(SkBlockAllocator::GrowthPolicy::kFixed) {} /** * Create an list that defaults to using StartingItems as the heap increment, but with * the defined growth policy. */ explicit SkTBlockList(SkBlockAllocator::GrowthPolicy policy) : SkTBlockList(StartingItems, policy) {} /** * Create an list. * * @param itemsPerBlock the number of items to allocate at once * @param policy the growth policy for subsequent blocks of items */ explicit SkTBlockList(int itemsPerBlock, SkBlockAllocator::GrowthPolicy policy = SkBlockAllocator::GrowthPolicy::kFixed) : fAllocator(policy, SkBlockAllocator::BlockOverhead() + sizeof(T)*itemsPerBlock) {} ~SkTBlockList() { this->reset(); } /** * Adds an item and returns it. * * @return the added item. */ T& push_back() { return *new (this->pushItem()) T; } T& push_back(const T& t) { return *new (this->pushItem()) T(t); } T& push_back(T&& t) { return *new (this->pushItem()) T(std::move(t)); } template T& emplace_back(Args&&... args) { return *new (this->pushItem()) T(std::forward(args)...); } /** * Move all items from 'other' to the end of this collection. When this returns, 'other' will * be empty. Items in 'other' may be moved as part of compacting the pre-allocated start of * 'other' into this list (using T's move constructor or memcpy if T is trivially copyable), but * this is O(StartingItems) and not O(N). All other items are concatenated in O(1). */ template void concat(SkTBlockList&& other); /** * Allocate, if needed, space to hold N more Ts before another malloc will occur. */ void reserve(int n) { int avail = fAllocator->currentBlock()->template avail() / 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( reserved * sizeof(T), SkBlockAllocator::kIgnoreExistingBytes_Flag); } } /** * Remove the last item, only call if count() != 0 */ void pop_back() { SkASSERT(this->count() > 0); SkBlockAllocator::Block* block = fAllocator->currentBlock(); // Run dtor for the popped item int releaseIndex = Last(block); GetItem(block, releaseIndex).~T(); if (releaseIndex == First(block)) { fAllocator->releaseBlock(block); } else { // Since this always follows LIFO, the block should always be able to release the memory SkAssertResult(block->release(releaseIndex, releaseIndex + sizeof(T))); block->setMetadata(Decrement(block, releaseIndex)); } fAllocator->setMetadata(fAllocator->metadata() - 1); } /** * Removes all added items. */ void reset() { // Invoke destructors in reverse order if not trivially destructible if constexpr (!std::is_trivially_destructible::value) { for (T& t : this->ritems()) { t.~T(); } } fAllocator->reset(); } /** * Returns the item count. */ int count() const { #ifdef SK_DEBUG // Confirm total count matches sum of block counts int count = 0; for (const auto* b :fAllocator->blocks()) { if (b->metadata() == 0) { continue; // skip empty } count += (sizeof(T) + Last(b) - First(b)) / sizeof(T); } SkASSERT(count == fAllocator->metadata()); #endif return fAllocator->metadata(); } /** * Is the count 0? */ bool empty() const { return this->count() == 0; } /** * Access first item, only call if count() != 0 */ T& front() { // This assumes that the head block actually have room to store the first item. static_assert(StartingItems >= 1); SkASSERT(this->count() > 0 && fAllocator->headBlock()->metadata() > 0); return GetItem(fAllocator->headBlock(), First(fAllocator->headBlock())); } const T& front() const { SkASSERT(this->count() > 0 && fAllocator->headBlock()->metadata() > 0); return GetItem(fAllocator->headBlock(), First(fAllocator->headBlock())); } /** * Access last item, only call if count() != 0 */ T& back() { SkASSERT(this->count() > 0 && fAllocator->currentBlock()->metadata() > 0); return GetItem(fAllocator->currentBlock(), Last(fAllocator->currentBlock())); } const T& back() const { SkASSERT(this->count() > 0 && fAllocator->currentBlock()->metadata() > 0); return GetItem(fAllocator->currentBlock(), Last(fAllocator->currentBlock())); } /** * Access item by index. Not an operator[] since it should not be considered constant time. * 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 < this->count()); // Iterate over blocks until we find the one that contains i. for (auto* b : fAllocator->blocks()) { if (b->metadata() == 0) { continue; // skip empty } int start = First(b); int end = Last(b) + sizeof(T); // exclusive int index = start + i * sizeof(T); if (index < end) { return GetItem(b, index); } else { i -= (end - start) / sizeof(T); } } SkUNREACHABLE; } const T& item(int i) const { return const_cast(this)->item(i); } private: // Let other SkTBlockLists have access (only ever used when T and S are the same but you // cannot have partial specializations declared as a friend...) template friend class SkTBlockList; friend class TBlockListTestAccess; // for fAllocator inline static constexpr size_t StartingSize = SkBlockAllocator::Overhead() + StartingItems * sizeof(T); static T& GetItem(SkBlockAllocator::Block* block, int index) { return *static_cast(block->ptr(index)); } static const T& GetItem(const SkBlockAllocator::Block* block, int index) { return *static_cast(block->ptr(index)); } static int First(const SkBlockAllocator::Block* b) { return b->firstAlignedOffset(); } static int Last(const SkBlockAllocator::Block* b) { return b->metadata(); } static int Increment(const SkBlockAllocator::Block* b, int index) { return index + sizeof(T); } static int Decrement(const SkBlockAllocator::Block* b, int index) { return index - sizeof(T); } void* pushItem() { // 'template' required because fAllocator is a template, calling a template member auto br = fAllocator->template allocate(sizeof(T)); SkASSERT(br.fStart == br.fAlignedOffset || br.fAlignedOffset == First(fAllocator->currentBlock())); br.fBlock->setMetadata(br.fAlignedOffset); fAllocator->setMetadata(fAllocator->metadata() + 1); return br.fBlock->ptr(br.fAlignedOffset); } // N represents the number of items, whereas SkSBlockAllocator takes total bytes, so must // account for the block allocator's size too. // // This class uses the SkBlockAllocator's metadata to track total count of items, and per-block // metadata to track the index of the last allocated item within each block. SkSBlockAllocator fAllocator; public: using Iter = BlockIndexIterator; using CIter = BlockIndexIterator; using RIter = BlockIndexIterator; using CRIter = BlockIndexIterator; /** * Iterate over all items in allocation order (oldest to newest) using a for-range loop: * * for (auto&& T : this->items()) {} */ Iter items() { return Iter(fAllocator.allocator()); } CIter items() const { return CIter(fAllocator.allocator()); } // Iterate from newest to oldest using a for-range loop. RIter ritems() { return RIter(fAllocator.allocator()); } CRIter ritems() const { return CRIter(fAllocator.allocator()); } }; template template void SkTBlockList::concat(SkTBlockList&& other) { // Optimize the common case where the list to append only has a single item if (other.empty()) { return; } else if (other.count() == 1) { this->push_back(other.back()); other.pop_back(); return; } // Manually move all items in other's head block into this list; all heap blocks from 'other' // will be appended to the block linked list (no per-item moves needed then). int headItemCount = 0; SkBlockAllocator::Block* headBlock = other.fAllocator->headBlock(); SkDEBUGCODE(int oldCount = this->count();) if (headBlock->metadata() > 0) { int headStart = First(headBlock); int headEnd = Last(headBlock) + sizeof(T); // exclusive headItemCount = (headEnd - headStart) / sizeof(T); int avail = fAllocator->currentBlock()->template avail() / sizeof(T); if (headItemCount > avail) { // Make sure there is extra room for the items beyond what's already avail. Use the // kIgnoreGrowthPolicy_Flag to make this reservation as tight as possible since // 'other's heap blocks will be appended after it and any extra space is wasted. fAllocator->template reserve((headItemCount - avail) * sizeof(T), SkBlockAllocator::kIgnoreExistingBytes_Flag | SkBlockAllocator::kIgnoreGrowthPolicy_Flag); } if constexpr (std::is_trivially_copy_constructible::value) { // memcpy all items at once (or twice between current and reserved space). SkASSERT(std::is_trivially_destructible::value); auto copy = [](SkBlockAllocator::Block* src, int start, SkBlockAllocator* dst, int n) { auto target = dst->template allocate(n * sizeof(T)); memcpy(target.fBlock->ptr(target.fAlignedOffset), src->ptr(start), n * sizeof(T)); target.fBlock->setMetadata(target.fAlignedOffset + (n - 1) * sizeof(T)); }; if (avail > 0) { // Copy 0 to avail items into existing tail block copy(headBlock, headStart, fAllocator.allocator(), std::min(headItemCount, avail)); } if (headItemCount > avail) { // Copy (head count - avail) into the extra reserved space copy(headBlock, headStart + avail * sizeof(T), fAllocator.allocator(), headItemCount - avail); } fAllocator->setMetadata(fAllocator->metadata() + headItemCount); } else { // Move every item over one at a time for (int i = headStart; i < headEnd; i += sizeof(T)) { T& toMove = GetItem(headBlock, i); this->push_back(std::move(toMove)); // Anything of interest should have been moved, but run this since T isn't // a trusted type. toMove.~T(); // NOLINT(bugprone-use-after-move): calling dtor always allowed } } other.fAllocator->releaseBlock(headBlock); } // other's head block must have been fully copied since it cannot be stolen SkASSERT(other.fAllocator->headBlock()->metadata() == 0 && fAllocator->metadata() == oldCount + headItemCount); fAllocator->stealHeapBlocks(other.fAllocator.allocator()); fAllocator->setMetadata(fAllocator->metadata() + (other.fAllocator->metadata() - headItemCount)); other.fAllocator->setMetadata(0); } /** * BlockIndexIterator provides a reusable iterator template for collections built on top of a * SkBlockAllocator, where each item is of the same type, and the index to an item can be iterated * over in a known manner. It supports const and non-const, and forward and reverse, assuming it's * provided with proper functions for starting, ending, and advancing. */ template ::type> Resolve> class BlockIndexIterator { using BlockIter = typename SkBlockAllocator::BlockIter; public: BlockIndexIterator(BlockIter iter) : fBlockIter(iter) {} class Item { public: bool operator!=(const Item& other) const { return other.fBlock != fBlock || (SkToBool(*fBlock) && other.fIndex != fIndex); } T operator*() const { SkASSERT(*fBlock); return Resolve(*fBlock, fIndex); } Item& operator++() { const auto* block = *fBlock; SkASSERT(block && block->metadata() > 0); SkASSERT((Forward && Next(block, fIndex) > fIndex) || (!Forward && Next(block, fIndex) < fIndex)); fIndex = Next(block, fIndex); if ((Forward && fIndex > fEndIndex) || (!Forward && fIndex < fEndIndex)) { ++fBlock; this->setIndices(); } return *this; } private: friend BlockIndexIterator; using BlockItem = typename BlockIter::Item; Item(BlockItem block) : fBlock(block) { this->setIndices(); } void setIndices() { // Skip empty blocks while(*fBlock && (*fBlock)->metadata() == 0) { ++fBlock; } if (*fBlock) { fIndex = Start(*fBlock); fEndIndex = End(*fBlock); } else { fIndex = 0; fEndIndex = 0; } SkASSERT((Forward && fIndex <= fEndIndex) || (!Forward && fIndex >= fEndIndex)); } BlockItem fBlock; int fIndex; int fEndIndex; }; Item begin() const { return Item(fBlockIter.begin()); } Item end() const { return Item(fBlockIter.end()); } private: BlockIter fBlockIter; }; #endif