xref: /aosp_15_r20/external/skia/src/base/SkTBlockList.h (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2010 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #ifndef SkTBlockList_DEFINED
9 #define SkTBlockList_DEFINED
10 
11 #include "include/private/base/SkAssert.h"
12 #include "include/private/base/SkDebug.h"
13 #include "include/private/base/SkTo.h"
14 #include "src/base/SkBlockAllocator.h"
15 
16 #include <algorithm>
17 #include <cstring>
18 #include <type_traits>
19 #include <utility>
20 
21 // Forward declarations for the iterators used by SkTBlockList
22 using IndexFn = int (*)(const SkBlockAllocator::Block*);
23 using NextFn = int (*)(const SkBlockAllocator::Block*, int);
24 template<typename T, typename B> using ItemFn = T (*)(B*, int);
25 template <typename T, bool Forward, bool Const, IndexFn Start, IndexFn End, NextFn Next,
26           ItemFn<T, typename std::conditional<Const, const SkBlockAllocator::Block,
27                                                      SkBlockAllocator::Block>::type> Resolve>
28 class BlockIndexIterator;
29 
30 /**
31  * SkTBlockList manages dynamic storage for instances of T, reserving fixed blocks such that
32  * allocation is amortized across every N instances. In this way it is a hybrid of an array-based
33  * vector and a linked-list. T can be any type and non-trivial destructors are automatically
34  * invoked when the SkTBlockList is destructed. The addresses of instances are guaranteed
35  * not to move except when a list is concatenated to another.
36  *
37  * The collection supports storing a templated number of elements inline before heap-allocated
38  * blocks are made to hold additional instances. By default, the heap blocks are sized to hold the
39  * same number of items as the inline block. A common pattern is to have the inline size hold only
40  * a small number of items for the common case and then allocate larger blocks when needed.
41  *
42  * If the size of a collection is N, and its block size is B, the complexity of the common
43  * operations are:
44  *  - push_back()/emplace_back(): O(1), with malloc O(B)
45  *  - pop_back(): O(1), with free O(B)
46  *  - front()/back(): O(1)
47  *  - reset(): O(N) for non-trivial types, O(N/B) for trivial types
48  *  - concat(): O(B)
49  *  - random access: O(N/B)
50  *  - iteration: O(1) at each step
51  *
52  * These characteristics make it well suited for allocating items in a LIFO ordering, or otherwise
53  * acting as a stack, or simply using it as a typed allocator.
54  */
55 template <typename T, int StartingItems = 1>
56 class SkTBlockList {
57 public:
58     /**
59      * Create an list that defaults to using StartingItems as heap increment and the
60      * kFixed growth policy (e.g. all allocations will match StartingItems).
61      */
SkTBlockList()62     SkTBlockList() : SkTBlockList(SkBlockAllocator::GrowthPolicy::kFixed) {}
63 
64     /**
65      * Create an list that defaults to using StartingItems as the heap increment, but with
66      * the defined growth policy.
67     */
SkTBlockList(SkBlockAllocator::GrowthPolicy policy)68     explicit SkTBlockList(SkBlockAllocator::GrowthPolicy policy)
69             : SkTBlockList(StartingItems, policy) {}
70 
71     /**
72      * Create an list.
73      *
74      * @param   itemsPerBlock   the number of items to allocate at once
75      * @param   policy          the growth policy for subsequent blocks of items
76      */
77     explicit SkTBlockList(int itemsPerBlock,
78                           SkBlockAllocator::GrowthPolicy policy =
79                                   SkBlockAllocator::GrowthPolicy::kFixed)
80             : fAllocator(policy,
81                          SkBlockAllocator::BlockOverhead<alignof(T)>() + sizeof(T)*itemsPerBlock) {}
82 
~SkTBlockList()83     ~SkTBlockList() { this->reset(); }
84 
85     /**
86      * Adds an item and returns it.
87      *
88      * @return the added item.
89      */
push_back()90     T& push_back() {
91         return *new (this->pushItem()) T;
92     }
push_back(const T & t)93     T& push_back(const T& t) {
94         return *new (this->pushItem()) T(t);
95     }
push_back(T && t)96     T& push_back(T&& t) {
97         return *new (this->pushItem()) T(std::move(t));
98     }
99 
100     template <typename... Args>
emplace_back(Args &&...args)101     T& emplace_back(Args&&... args) {
102         return *new (this->pushItem()) T(std::forward<Args>(args)...);
103     }
104 
105     /**
106      * Move all items from 'other' to the end of this collection. When this returns, 'other' will
107      * be empty. Items in 'other' may be moved as part of compacting the pre-allocated start of
108      * 'other' into this list (using T's move constructor or memcpy if T is trivially copyable), but
109      * this is O(StartingItems) and not O(N). All other items are concatenated in O(1).
110      */
111     template <int SI>
112     void concat(SkTBlockList<T, SI>&& other);
113 
114     /**
115      * Allocate, if needed, space to hold N more Ts before another malloc will occur.
116      */
reserve(int n)117     void reserve(int n) {
118         int avail = fAllocator->currentBlock()->template avail<alignof(T)>() / sizeof(T);
119         if (n > avail) {
120             int reserved = n - avail;
121             // Don't consider existing bytes since we've already determined how to split the N items
122             fAllocator->template reserve<alignof(T)>(
123                     reserved * sizeof(T), SkBlockAllocator::kIgnoreExistingBytes_Flag);
124         }
125     }
126 
127     /**
128      * Remove the last item, only call if count() != 0
129      */
pop_back()130     void pop_back() {
131         SkASSERT(this->count() > 0);
132 
133         SkBlockAllocator::Block* block = fAllocator->currentBlock();
134 
135         // Run dtor for the popped item
136         int releaseIndex = Last(block);
137         GetItem(block, releaseIndex).~T();
138 
139         if (releaseIndex == First(block)) {
140             fAllocator->releaseBlock(block);
141         } else {
142             // Since this always follows LIFO, the block should always be able to release the memory
143             SkAssertResult(block->release(releaseIndex, releaseIndex + sizeof(T)));
144             block->setMetadata(Decrement(block, releaseIndex));
145         }
146 
147         fAllocator->setMetadata(fAllocator->metadata() - 1);
148     }
149 
150     /**
151      * Removes all added items.
152      */
reset()153     void reset() {
154         // Invoke destructors in reverse order if not trivially destructible
155         if constexpr (!std::is_trivially_destructible<T>::value) {
156             for (T& t : this->ritems()) {
157                 t.~T();
158             }
159         }
160 
161         fAllocator->reset();
162     }
163 
164     /**
165      * Returns the item count.
166      */
count()167     int count() const {
168 #ifdef SK_DEBUG
169         // Confirm total count matches sum of block counts
170         int count = 0;
171         for (const auto* b :fAllocator->blocks()) {
172             if (b->metadata() == 0) {
173                 continue; // skip empty
174             }
175             count += (sizeof(T) + Last(b) - First(b)) / sizeof(T);
176         }
177         SkASSERT(count == fAllocator->metadata());
178 #endif
179         return fAllocator->metadata();
180     }
181 
182     /**
183      * Is the count 0?
184      */
empty()185     bool empty() const { return this->count() == 0; }
186 
187     /**
188      * Access first item, only call if count() != 0
189      */
front()190     T& front() {
191         // This assumes that the head block actually have room to store the first item.
192         static_assert(StartingItems >= 1);
193         SkASSERT(this->count() > 0 && fAllocator->headBlock()->metadata() > 0);
194         return GetItem(fAllocator->headBlock(), First(fAllocator->headBlock()));
195     }
front()196     const T& front() const {
197         SkASSERT(this->count() > 0 && fAllocator->headBlock()->metadata() > 0);
198         return GetItem(fAllocator->headBlock(), First(fAllocator->headBlock()));
199     }
200 
201     /**
202      * Access last item, only call if count() != 0
203      */
back()204     T& back() {
205         SkASSERT(this->count() > 0 && fAllocator->currentBlock()->metadata() > 0);
206         return GetItem(fAllocator->currentBlock(), Last(fAllocator->currentBlock()));
207     }
back()208     const T& back() const {
209         SkASSERT(this->count() > 0 && fAllocator->currentBlock()->metadata() > 0);
210         return GetItem(fAllocator->currentBlock(), Last(fAllocator->currentBlock()));
211     }
212 
213     /**
214      * Access item by index. Not an operator[] since it should not be considered constant time.
215      * Use for-range loops by calling items() or ritems() instead to access all added items in order
216      */
item(int i)217     T& item(int i) {
218         SkASSERT(i >= 0 && i < this->count());
219 
220         // Iterate over blocks until we find the one that contains i.
221         for (auto* b : fAllocator->blocks()) {
222             if (b->metadata() == 0) {
223                 continue; // skip empty
224             }
225 
226             int start = First(b);
227             int end = Last(b) + sizeof(T); // exclusive
228             int index = start + i * sizeof(T);
229             if (index < end) {
230                 return GetItem(b, index);
231             } else {
232                 i -= (end - start) / sizeof(T);
233             }
234         }
235         SkUNREACHABLE;
236     }
item(int i)237     const T& item(int i) const {
238         return const_cast<SkTBlockList*>(this)->item(i);
239     }
240 
241 private:
242     // Let other SkTBlockLists have access (only ever used when T and S are the same but you
243     // cannot have partial specializations declared as a friend...)
244     template<typename S, int N> friend class SkTBlockList;
245     friend class TBlockListTestAccess;  // for fAllocator
246 
247     inline static constexpr size_t StartingSize =
248             SkBlockAllocator::Overhead<alignof(T)>() + StartingItems * sizeof(T);
249 
GetItem(SkBlockAllocator::Block * block,int index)250     static T& GetItem(SkBlockAllocator::Block* block, int index) {
251         return *static_cast<T*>(block->ptr(index));
252     }
GetItem(const SkBlockAllocator::Block * block,int index)253     static const T& GetItem(const SkBlockAllocator::Block* block, int index) {
254         return *static_cast<const T*>(block->ptr(index));
255     }
First(const SkBlockAllocator::Block * b)256     static int First(const SkBlockAllocator::Block* b) {
257         return b->firstAlignedOffset<alignof(T)>();
258     }
Last(const SkBlockAllocator::Block * b)259     static int Last(const SkBlockAllocator::Block* b) {
260         return b->metadata();
261     }
Increment(const SkBlockAllocator::Block * b,int index)262     static int Increment(const SkBlockAllocator::Block* b, int index) {
263         return index + sizeof(T);
264     }
Decrement(const SkBlockAllocator::Block * b,int index)265     static int Decrement(const SkBlockAllocator::Block* b, int index) {
266         return index - sizeof(T);
267     }
268 
pushItem()269     void* pushItem() {
270         // 'template' required because fAllocator is a template, calling a template member
271         auto br = fAllocator->template allocate<alignof(T)>(sizeof(T));
272         SkASSERT(br.fStart == br.fAlignedOffset ||
273                  br.fAlignedOffset == First(fAllocator->currentBlock()));
274         br.fBlock->setMetadata(br.fAlignedOffset);
275         fAllocator->setMetadata(fAllocator->metadata() + 1);
276         return br.fBlock->ptr(br.fAlignedOffset);
277     }
278 
279     // N represents the number of items, whereas SkSBlockAllocator takes total bytes, so must
280     // account for the block allocator's size too.
281     //
282     // This class uses the SkBlockAllocator's metadata to track total count of items, and per-block
283     // metadata to track the index of the last allocated item within each block.
284     SkSBlockAllocator<StartingSize> fAllocator;
285 
286 public:
287     using Iter   = BlockIndexIterator<T&,       true,  false, &First, &Last,  &Increment, &GetItem>;
288     using CIter  = BlockIndexIterator<const T&, true,  true,  &First, &Last,  &Increment, &GetItem>;
289     using RIter  = BlockIndexIterator<T&,       false, false, &Last,  &First, &Decrement, &GetItem>;
290     using CRIter = BlockIndexIterator<const T&, false, true,  &Last,  &First, &Decrement, &GetItem>;
291 
292     /**
293      * Iterate over all items in allocation order (oldest to newest) using a for-range loop:
294      *
295      *   for (auto&& T : this->items()) {}
296      */
items()297     Iter   items() { return Iter(fAllocator.allocator()); }
items()298     CIter  items() const { return CIter(fAllocator.allocator()); }
299 
300     // Iterate from newest to oldest using a for-range loop.
ritems()301     RIter  ritems() { return RIter(fAllocator.allocator()); }
ritems()302     CRIter ritems() const { return CRIter(fAllocator.allocator()); }
303 };
304 
305 template <typename T, int SI1>
306 template <int SI2>
concat(SkTBlockList<T,SI2> && other)307 void SkTBlockList<T, SI1>::concat(SkTBlockList<T, SI2>&& other) {
308     // Optimize the common case where the list to append only has a single item
309     if (other.empty()) {
310         return;
311     } else if (other.count() == 1) {
312         this->push_back(other.back());
313         other.pop_back();
314         return;
315     }
316 
317     // Manually move all items in other's head block into this list; all heap blocks from 'other'
318     // will be appended to the block linked list (no per-item moves needed then).
319     int headItemCount = 0;
320     SkBlockAllocator::Block* headBlock = other.fAllocator->headBlock();
321     SkDEBUGCODE(int oldCount = this->count();)
322     if (headBlock->metadata() > 0) {
323         int headStart = First(headBlock);
324         int headEnd = Last(headBlock) + sizeof(T); // exclusive
325         headItemCount = (headEnd - headStart) / sizeof(T);
326         int avail = fAllocator->currentBlock()->template avail<alignof(T)>() / sizeof(T);
327         if (headItemCount > avail) {
328             // Make sure there is extra room for the items beyond what's already avail. Use the
329             // kIgnoreGrowthPolicy_Flag to make this reservation as tight as possible since
330             // 'other's heap blocks will be appended after it and any extra space is wasted.
331             fAllocator->template reserve<alignof(T)>((headItemCount - avail) * sizeof(T),
332                                                      SkBlockAllocator::kIgnoreExistingBytes_Flag |
333                                                      SkBlockAllocator::kIgnoreGrowthPolicy_Flag);
334         }
335 
336         if constexpr (std::is_trivially_copy_constructible<T>::value) {
337             // memcpy all items at once (or twice between current and reserved space).
338             SkASSERT(std::is_trivially_destructible<T>::value);
339             auto copy = [](SkBlockAllocator::Block* src, int start, SkBlockAllocator* dst, int n) {
340                 auto target = dst->template allocate<alignof(T)>(n * sizeof(T));
341                 memcpy(target.fBlock->ptr(target.fAlignedOffset), src->ptr(start), n * sizeof(T));
342                 target.fBlock->setMetadata(target.fAlignedOffset + (n - 1) * sizeof(T));
343             };
344 
345             if (avail > 0) {
346                 // Copy 0 to avail items into existing tail block
347                 copy(headBlock, headStart, fAllocator.allocator(), std::min(headItemCount, avail));
348             }
349             if (headItemCount > avail) {
350                 // Copy (head count - avail) into the extra reserved space
351                 copy(headBlock, headStart + avail * sizeof(T),
352                      fAllocator.allocator(), headItemCount - avail);
353             }
354             fAllocator->setMetadata(fAllocator->metadata() + headItemCount);
355         } else {
356             // Move every item over one at a time
357             for (int i = headStart; i < headEnd; i += sizeof(T)) {
358                 T& toMove = GetItem(headBlock, i);
359                 this->push_back(std::move(toMove));
360                 // Anything of interest should have been moved, but run this since T isn't
361                 // a trusted type.
362                 toMove.~T(); // NOLINT(bugprone-use-after-move): calling dtor always allowed
363             }
364         }
365 
366         other.fAllocator->releaseBlock(headBlock);
367     }
368 
369     // other's head block must have been fully copied since it cannot be stolen
370     SkASSERT(other.fAllocator->headBlock()->metadata() == 0 &&
371              fAllocator->metadata() == oldCount + headItemCount);
372     fAllocator->stealHeapBlocks(other.fAllocator.allocator());
373     fAllocator->setMetadata(fAllocator->metadata() +
374                             (other.fAllocator->metadata() - headItemCount));
375     other.fAllocator->setMetadata(0);
376 }
377 
378 /**
379  * BlockIndexIterator provides a reusable iterator template for collections built on top of a
380  * SkBlockAllocator, where each item is of the same type, and the index to an item can be iterated
381  * over in a known manner. It supports const and non-const, and forward and reverse, assuming it's
382  * provided with proper functions for starting, ending, and advancing.
383  */
384 template <typename T,    // The element type (including any modifiers)
385           bool Forward,  // Are indices within a block increasing or decreasing with iteration?
386           bool Const,    // Whether or not T is const
387           IndexFn Start, // Returns the index of the first valid item in a block
388           IndexFn End,   // Returns the index of the last valid item (so it is inclusive)
389           NextFn Next,   // Returns the next index given the current index
390           ItemFn<T, typename std::conditional<Const, const SkBlockAllocator::Block,
391                                                      SkBlockAllocator::Block>::type> Resolve>
392 class BlockIndexIterator {
393     using BlockIter = typename SkBlockAllocator::BlockIter<Forward, Const>;
394 public:
BlockIndexIterator(BlockIter iter)395     BlockIndexIterator(BlockIter iter) : fBlockIter(iter) {}
396 
397     class Item {
398     public:
399         bool operator!=(const Item& other) const {
400             return other.fBlock != fBlock || (SkToBool(*fBlock) && other.fIndex != fIndex);
401         }
402 
403         T operator*() const {
404             SkASSERT(*fBlock);
405             return Resolve(*fBlock, fIndex);
406         }
407 
408         Item& operator++() {
409             const auto* block = *fBlock;
410             SkASSERT(block && block->metadata() > 0);
411             SkASSERT((Forward && Next(block, fIndex) > fIndex) ||
412                      (!Forward && Next(block, fIndex) < fIndex));
413             fIndex = Next(block, fIndex);
414             if ((Forward && fIndex > fEndIndex) || (!Forward && fIndex < fEndIndex)) {
415                 ++fBlock;
416                 this->setIndices();
417             }
418             return *this;
419         }
420 
421     private:
422         friend BlockIndexIterator;
423         using BlockItem = typename BlockIter::Item;
424 
Item(BlockItem block)425         Item(BlockItem block) : fBlock(block) {
426             this->setIndices();
427         }
428 
setIndices()429         void setIndices() {
430             // Skip empty blocks
431             while(*fBlock && (*fBlock)->metadata() == 0) {
432                 ++fBlock;
433             }
434             if (*fBlock) {
435                 fIndex = Start(*fBlock);
436                 fEndIndex = End(*fBlock);
437             } else {
438                 fIndex = 0;
439                 fEndIndex = 0;
440             }
441 
442             SkASSERT((Forward && fIndex <= fEndIndex) || (!Forward && fIndex >= fEndIndex));
443         }
444 
445         BlockItem fBlock;
446         int       fIndex;
447         int       fEndIndex;
448     };
449 
begin()450     Item begin() const { return Item(fBlockIter.begin()); }
end()451     Item end() const { return Item(fBlockIter.end()); }
452 
453 private:
454     BlockIter fBlockIter;
455 };
456 
457 #endif
458