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