Swap a few APIs to reduce sign and implicit truncations required to work with it
[folly.git] / folly / IndexedMemPool.h
1 /*
2  * Copyright 2017 Facebook, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #pragma once
18
19 #include <type_traits>
20 #include <assert.h>
21 #include <errno.h>
22 #include <stdint.h>
23 #include <boost/noncopyable.hpp>
24 #include <folly/AtomicStruct.h>
25 #include <folly/detail/CacheLocality.h>
26 #include <folly/portability/SysMman.h>
27 #include <folly/portability/Unistd.h>
28
29 // Ignore shadowing warnings within this file, so includers can use -Wshadow.
30 #pragma GCC diagnostic push
31 #pragma GCC diagnostic ignored "-Wshadow"
32
33 namespace folly {
34
35 namespace detail {
36 template <typename Pool>
37 struct IndexedMemPoolRecycler;
38 }
39
40 /// Instances of IndexedMemPool dynamically allocate and then pool their
41 /// element type (T), returning 4-byte integer indices that can be passed
42 /// to the pool's operator[] method to access or obtain pointers to the
43 /// actual elements.  The memory backing items returned from the pool
44 /// will always be readable, even if items have been returned to the pool.
45 /// These two features are useful for lock-free algorithms.  The indexing
46 /// behavior makes it easy to build tagged pointer-like-things, since
47 /// a large number of elements can be managed using fewer bits than a
48 /// full pointer.  The access-after-free behavior makes it safe to read
49 /// from T-s even after they have been recycled, since it is guaranteed
50 /// that the memory won't have been returned to the OS and unmapped
51 /// (the algorithm must still use a mechanism to validate that the read
52 /// was correct, but it doesn't have to worry about page faults), and if
53 /// the elements use internal sequence numbers it can be guaranteed that
54 /// there won't be an ABA match due to the element being overwritten with
55 /// a different type that has the same bit pattern.
56 ///
57 /// IndexedMemPool has two object lifecycle strategies.  The first
58 /// is to construct objects when they are allocated from the pool and
59 /// destroy them when they are recycled.  In this mode allocIndex and
60 /// allocElem have emplace-like semantics.  In the second mode, objects
61 /// are default-constructed the first time they are removed from the pool,
62 /// and deleted when the pool itself is deleted.  By default the first
63 /// mode is used for non-trivial T, and the second is used for trivial T.
64 ///
65 /// IMPORTANT: Space for extra elements is allocated to account for those
66 /// that are inaccessible because they are in other local lists, so the
67 /// actual number of items that can be allocated ranges from capacity to
68 /// capacity + (NumLocalLists_-1)*LocalListLimit_.  This is important if
69 /// you are trying to maximize the capacity of the pool while constraining
70 /// the bit size of the resulting pointers, because the pointers will
71 /// actually range up to the boosted capacity.  See maxIndexForCapacity
72 /// and capacityForMaxIndex.
73 ///
74 /// To avoid contention, NumLocalLists_ free lists of limited (less than
75 /// or equal to LocalListLimit_) size are maintained, and each thread
76 /// retrieves and returns entries from its associated local list.  If the
77 /// local list becomes too large then elements are placed in bulk in a
78 /// global free list.  This allows items to be efficiently recirculated
79 /// from consumers to producers.  AccessSpreader is used to access the
80 /// local lists, so there is no performance advantage to having more
81 /// local lists than L1 caches.
82 ///
83 /// The pool mmap-s the entire necessary address space when the pool is
84 /// constructed, but delays element construction.  This means that only
85 /// elements that are actually returned to the caller get paged into the
86 /// process's resident set (RSS).
87 template <
88     typename T,
89     uint32_t NumLocalLists_ = 32,
90     uint32_t LocalListLimit_ = 200,
91     template <typename> class Atom = std::atomic,
92     bool EagerRecycleWhenTrivial = false,
93     bool EagerRecycleWhenNotTrivial = true>
94 struct IndexedMemPool : boost::noncopyable {
95   typedef T value_type;
96
97   typedef std::unique_ptr<T, detail::IndexedMemPoolRecycler<IndexedMemPool>>
98       UniquePtr;
99
100   static_assert(LocalListLimit_ <= 255, "LocalListLimit must fit in 8 bits");
101   enum {
102     NumLocalLists = NumLocalLists_,
103     LocalListLimit = LocalListLimit_
104   };
105
106
107   static constexpr bool eagerRecycle() {
108     return std::is_trivial<T>::value
109         ? EagerRecycleWhenTrivial : EagerRecycleWhenNotTrivial;
110   }
111
112   // these are public because clients may need to reason about the number
113   // of bits required to hold indices from a pool, given its capacity
114
115   static constexpr uint32_t maxIndexForCapacity(uint32_t capacity) {
116     // index of std::numeric_limits<uint32_t>::max() is reserved for isAllocated
117     // tracking
118     return uint32_t(std::min(
119         uint64_t(capacity) + (NumLocalLists - 1) * LocalListLimit,
120         uint64_t(std::numeric_limits<uint32_t>::max() - 1)));
121   }
122
123   static constexpr uint32_t capacityForMaxIndex(uint32_t maxIndex) {
124     return maxIndex - (NumLocalLists - 1) * LocalListLimit;
125   }
126
127
128   /// Constructs a pool that can allocate at least _capacity_ elements,
129   /// even if all the local lists are full
130   explicit IndexedMemPool(uint32_t capacity)
131     : actualCapacity_(maxIndexForCapacity(capacity))
132     , size_(0)
133     , globalHead_(TaggedPtr{})
134   {
135     const size_t needed = sizeof(Slot) * (actualCapacity_ + 1);
136     size_t pagesize = size_t(sysconf(_SC_PAGESIZE));
137     mmapLength_ = ((needed - 1) & ~(pagesize - 1)) + pagesize;
138     assert(needed <= mmapLength_ && mmapLength_ < needed + pagesize);
139     assert((mmapLength_ % pagesize) == 0);
140
141     slots_ = static_cast<Slot*>(mmap(nullptr, mmapLength_,
142                                      PROT_READ | PROT_WRITE,
143                                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
144     if (slots_ == MAP_FAILED) {
145       assert(errno == ENOMEM);
146       throw std::bad_alloc();
147     }
148   }
149
150   /// Destroys all of the contained elements
151   ~IndexedMemPool() {
152     if (!eagerRecycle()) {
153       for (uint32_t i = size_; i > 0; --i) {
154         slots_[i].~Slot();
155       }
156     }
157     munmap(slots_, mmapLength_);
158   }
159
160   /// Returns a lower bound on the number of elements that may be
161   /// simultaneously allocated and not yet recycled.  Because of the
162   /// local lists it is possible that more elements than this are returned
163   /// successfully
164   uint32_t capacity() {
165     return capacityForMaxIndex(actualCapacity_);
166   }
167
168   /// Finds a slot with a non-zero index, emplaces a T there if we're
169   /// using the eager recycle lifecycle mode, and returns the index,
170   /// or returns 0 if no elements are available.
171   template <typename ...Args>
172   uint32_t allocIndex(Args&&... args) {
173     static_assert(sizeof...(Args) == 0 || eagerRecycle(),
174         "emplace-style allocation requires eager recycle, "
175         "which is defaulted only for non-trivial types");
176     auto idx = localPop(localHead());
177     if (idx != 0 && eagerRecycle()) {
178       T* ptr = &slot(idx).elem;
179       new (ptr) T(std::forward<Args>(args)...);
180     }
181     return idx;
182   }
183
184   /// If an element is available, returns a std::unique_ptr to it that will
185   /// recycle the element to the pool when it is reclaimed, otherwise returns
186   /// a null (falsy) std::unique_ptr
187   template <typename ...Args>
188   UniquePtr allocElem(Args&&... args) {
189     auto idx = allocIndex(std::forward<Args>(args)...);
190     T* ptr = idx == 0 ? nullptr : &slot(idx).elem;
191     return UniquePtr(ptr, typename UniquePtr::deleter_type(this));
192   }
193
194   /// Gives up ownership previously granted by alloc()
195   void recycleIndex(uint32_t idx) {
196     assert(isAllocated(idx));
197     if (eagerRecycle()) {
198       slot(idx).elem.~T();
199     }
200     localPush(localHead(), idx);
201   }
202
203   /// Provides access to the pooled element referenced by idx
204   T& operator[](uint32_t idx) {
205     return slot(idx).elem;
206   }
207
208   /// Provides access to the pooled element referenced by idx
209   const T& operator[](uint32_t idx) const {
210     return slot(idx).elem;
211   }
212
213   /// If elem == &pool[idx], then pool.locateElem(elem) == idx.  Also,
214   /// pool.locateElem(nullptr) == 0
215   uint32_t locateElem(const T* elem) const {
216     if (!elem) {
217       return 0;
218     }
219
220     static_assert(std::is_standard_layout<Slot>::value, "offsetof needs POD");
221
222     auto slot = reinterpret_cast<const Slot*>(
223         reinterpret_cast<const char*>(elem) - offsetof(Slot, elem));
224     auto rv = uint32_t(slot - slots_);
225
226     // this assert also tests that rv is in range
227     assert(elem == &(*this)[rv]);
228     return rv;
229   }
230
231   /// Returns true iff idx has been alloc()ed and not recycleIndex()ed
232   bool isAllocated(uint32_t idx) const {
233     return slot(idx).localNext == uint32_t(-1);
234   }
235
236
237  private:
238   ///////////// types
239
240   struct Slot {
241     T elem;
242     uint32_t localNext;
243     uint32_t globalNext;
244
245     Slot() : localNext{}, globalNext{} {}
246   };
247
248   struct TaggedPtr {
249     uint32_t idx;
250
251     // size is bottom 8 bits, tag in top 24.  g++'s code generation for
252     // bitfields seems to depend on the phase of the moon, plus we can
253     // do better because we can rely on other checks to avoid masking
254     uint32_t tagAndSize;
255
256     enum : uint32_t {
257         SizeBits = 8,
258         SizeMask = (1U << SizeBits) - 1,
259         TagIncr = 1U << SizeBits,
260     };
261
262     uint32_t size() const {
263       return tagAndSize & SizeMask;
264     }
265
266     TaggedPtr withSize(uint32_t repl) const {
267       assert(repl <= LocalListLimit);
268       return TaggedPtr{ idx, (tagAndSize & ~SizeMask) | repl };
269     }
270
271     TaggedPtr withSizeIncr() const {
272       assert(size() < LocalListLimit);
273       return TaggedPtr{ idx, tagAndSize + 1 };
274     }
275
276     TaggedPtr withSizeDecr() const {
277       assert(size() > 0);
278       return TaggedPtr{ idx, tagAndSize - 1 };
279     }
280
281     TaggedPtr withIdx(uint32_t repl) const {
282       return TaggedPtr{ repl, tagAndSize + TagIncr };
283     }
284
285     TaggedPtr withEmpty() const {
286       return withIdx(0).withSize(0);
287     }
288   };
289
290   struct FOLLY_ALIGN_TO_AVOID_FALSE_SHARING LocalList {
291     AtomicStruct<TaggedPtr,Atom> head;
292
293     LocalList() : head(TaggedPtr{}) {}
294   };
295
296   ////////// fields
297
298   /// the number of bytes allocated from mmap, which is a multiple of
299   /// the page size of the machine
300   size_t mmapLength_;
301
302   /// the actual number of slots that we will allocate, to guarantee
303   /// that we will satisfy the capacity requested at construction time.
304   /// They will be numbered 1..actualCapacity_ (note the 1-based counting),
305   /// and occupy slots_[1..actualCapacity_].
306   uint32_t actualCapacity_;
307
308   /// this records the number of slots that have actually been constructed.
309   /// To allow use of atomic ++ instead of CAS, we let this overflow.
310   /// The actual number of constructed elements is min(actualCapacity_,
311   /// size_)
312   Atom<uint32_t> size_;
313
314   /// raw storage, only 1..min(size_,actualCapacity_) (inclusive) are
315   /// actually constructed.  Note that slots_[0] is not constructed or used
316   FOLLY_ALIGN_TO_AVOID_FALSE_SHARING Slot* slots_;
317
318   /// use AccessSpreader to find your list.  We use stripes instead of
319   /// thread-local to avoid the need to grow or shrink on thread start
320   /// or join.   These are heads of lists chained with localNext
321   LocalList local_[NumLocalLists];
322
323   /// this is the head of a list of node chained by globalNext, that are
324   /// themselves each the head of a list chained by localNext
325   FOLLY_ALIGN_TO_AVOID_FALSE_SHARING AtomicStruct<TaggedPtr,Atom> globalHead_;
326
327   ///////////// private methods
328
329   uint32_t slotIndex(uint32_t idx) const {
330     assert(0 < idx &&
331            idx <= actualCapacity_ &&
332            idx <= size_.load(std::memory_order_acquire));
333     return idx;
334   }
335
336   Slot& slot(uint32_t idx) {
337     return slots_[slotIndex(idx)];
338   }
339
340   const Slot& slot(uint32_t idx) const {
341     return slots_[slotIndex(idx)];
342   }
343
344   // localHead references a full list chained by localNext.  s should
345   // reference slot(localHead), it is passed as a micro-optimization
346   void globalPush(Slot& s, uint32_t localHead) {
347     while (true) {
348       TaggedPtr gh = globalHead_.load(std::memory_order_acquire);
349       s.globalNext = gh.idx;
350       if (globalHead_.compare_exchange_strong(gh, gh.withIdx(localHead))) {
351         // success
352         return;
353       }
354     }
355   }
356
357   // idx references a single node
358   void localPush(AtomicStruct<TaggedPtr,Atom>& head, uint32_t idx) {
359     Slot& s = slot(idx);
360     TaggedPtr h = head.load(std::memory_order_acquire);
361     while (true) {
362       s.localNext = h.idx;
363
364       if (h.size() == LocalListLimit) {
365         // push will overflow local list, steal it instead
366         if (head.compare_exchange_strong(h, h.withEmpty())) {
367           // steal was successful, put everything in the global list
368           globalPush(s, idx);
369           return;
370         }
371       } else {
372         // local list has space
373         if (head.compare_exchange_strong(h, h.withIdx(idx).withSizeIncr())) {
374           // success
375           return;
376         }
377       }
378       // h was updated by failing CAS
379     }
380   }
381
382   // returns 0 if empty
383   uint32_t globalPop() {
384     while (true) {
385       TaggedPtr gh = globalHead_.load(std::memory_order_acquire);
386       if (gh.idx == 0 || globalHead_.compare_exchange_strong(
387                   gh, gh.withIdx(slot(gh.idx).globalNext))) {
388         // global list is empty, or pop was successful
389         return gh.idx;
390       }
391     }
392   }
393
394   // returns 0 if allocation failed
395   uint32_t localPop(AtomicStruct<TaggedPtr,Atom>& head) {
396     while (true) {
397       TaggedPtr h = head.load(std::memory_order_acquire);
398       if (h.idx != 0) {
399         // local list is non-empty, try to pop
400         Slot& s = slot(h.idx);
401         if (head.compare_exchange_strong(
402                     h, h.withIdx(s.localNext).withSizeDecr())) {
403           // success
404           s.localNext = uint32_t(-1);
405           return h.idx;
406         }
407         continue;
408       }
409
410       uint32_t idx = globalPop();
411       if (idx == 0) {
412         // global list is empty, allocate and construct new slot
413         if (size_.load(std::memory_order_relaxed) >= actualCapacity_ ||
414             (idx = ++size_) > actualCapacity_) {
415           // allocation failed
416           return 0;
417         }
418         // default-construct it now if we aren't going to construct and
419         // destroy on each allocation
420         if (!eagerRecycle()) {
421           T* ptr = &slot(idx).elem;
422           new (ptr) T();
423         }
424         slot(idx).localNext = uint32_t(-1);
425         return idx;
426       }
427
428       Slot& s = slot(idx);
429       if (head.compare_exchange_strong(
430                   h, h.withIdx(s.localNext).withSize(LocalListLimit))) {
431         // global list moved to local list, keep head for us
432         s.localNext = uint32_t(-1);
433         return idx;
434       }
435       // local bulk push failed, return idx to the global list and try again
436       globalPush(s, idx);
437     }
438   }
439
440   AtomicStruct<TaggedPtr,Atom>& localHead() {
441     auto stripe = detail::AccessSpreader<Atom>::current(NumLocalLists);
442     return local_[stripe].head;
443   }
444 };
445
446 namespace detail {
447
448 /// This is a stateful Deleter functor, which allows std::unique_ptr
449 /// to track elements allocated from an IndexedMemPool by tracking the
450 /// associated pool.  See IndexedMemPool::allocElem.
451 template <typename Pool>
452 struct IndexedMemPoolRecycler {
453   Pool* pool;
454
455   explicit IndexedMemPoolRecycler(Pool* pool) : pool(pool) {}
456
457   IndexedMemPoolRecycler(const IndexedMemPoolRecycler<Pool>& rhs)
458       = default;
459   IndexedMemPoolRecycler& operator= (const IndexedMemPoolRecycler<Pool>& rhs)
460       = default;
461
462   void operator()(typename Pool::value_type* elem) const {
463     pool->recycleIndex(pool->locateElem(elem));
464   }
465 };
466
467 }
468
469 } // namespace folly
470
471 # pragma GCC diagnostic pop