Seastar
High performance C++ framework for concurrent servers
chunked_fifo.hh
1 /*
2  * This file is open source software, licensed to you under the terms
3  * of the Apache License, Version 2.0 (the "License"). See the NOTICE file
4  * distributed with this work for additional information regarding copyright
5  * ownership. You may not use this file except in compliance with the License.
6  *
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing,
12  * software distributed under the License is distributed on an
13  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14  * KIND, either express or implied. See the License for the
15  * specific language governing permissions and limitations
16  * under the License.
17  */
18 /*
19  * Copyright (C) 2016 ScyllaDB Ltd.
20  */
21 
22 #pragma once
23 
24 #ifndef SEASTAR_MODULE
25 #include <memory>
26 #include <algorithm>
27 #include <type_traits>
28 #include <seastar/util/modules.hh>
29 #endif
30 
31 namespace seastar {
32 
33 // An unbounded FIFO queue of objects of type T.
34 //
35 // It provides operations to push items in one end of the queue, and pop them
36 // from the other end of the queue - both operations are guaranteed O(1)
37 // (not just amortized O(1)). The size() operation is also O(1).
38 // chunked_fifo also guarantees that the largest contiguous memory allocation
39 // it does is O(1). The total memory used is, of course, O(N).
40 //
41 // How does chunked_fifo differ from std::list<>, circular_buffer<> and
42 // std::deque()?
43 //
44 // std::list<> can also make all the above guarantees, but is inefficient -
45 // both at run speed (every operation requires an allocation), and in memory
46 // use. Much more efficient than std::list<> is our circular_buffer<>, which
47 // allocates a contiguous array to hold the items and only reallocates it,
48 // exponentially, when the queue grows. On one test of several different
49 // push/pop scenarios, circular_buffer<> was between 5 and 20 times faster
50 // than std::list, and also used considerably less memory.
51 // The problem with circular_buffer<> is that gives up on the last guarantee
52 // we made above: circular_buffer<> allocates all the items in one large
53 // contiguous allocation - that might not be possible when the memory is
54 // highly fragmented.
55 // std::deque<> aims to solve the contiguous allocation problem by allocating
56 // smaller chunks of the queue, and keeping a list of them in an array. This
57 // array is necessary to allow for O(1) random access to any element, a
58 // feature which we do not need; But this array is itself contiguous so
59 // std::deque<> attempts larger contiguous allocations the larger the queue
60 // gets: std::deque<>'s contiguous allocation is still O(N) and in fact
61 // exactly 1/64 of the size of circular_buffer<>'s contiguous allocation.
62 // So it's an improvement over circular_buffer<>, but not a full solution.
63 //
64 // chunked_fifo<> is such a solution: it also allocates the queue in fixed-
65 // size chunks (just like std::deque) but holds them in a linked list, not
66 // a contiguous array, so there are no large contiguous allocations.
67 //
68 // Unlike std::deque<> or circular_buffer<>, chunked_fifo only provides the
69 // operations needed by std::queue, i.e.,: empty(), size(), front(), back(),
70 // push_back() and pop_front(). For simplicity, we do *not* implement other
71 // possible operations, like inserting or deleting elements from the "wrong"
72 // side of the queue or from the middle, nor random-access to items in the
73 // middle of the queue. However, chunked_fifo does allow iterating over all
74 // of the queue's elements without popping them, a feature which std::queue
75 // is missing.
76 //
77 // Another feature of chunked_fifo which std::deque is missing is the ability
78 // to control the chunk size, as a template parameter. In std::deque the
79 // chunk size is undocumented and fixed - in gcc, it is always 512 bytes.
80 // chunked_fifo, on the other hand, makes the chunk size (in number of items
81 // instead of bytes) a template parameter; In situations where the queue is
82 // expected to become very long, using a larger chunk size might make sense
83 // because it will result in fewer allocations.
84 //
85 // chunked_fifo uses uninitialized storage for unoccupied elements, and thus
86 // uses move/copy constructors instead of move/copy assignments, which are
87 // less efficient.
88 
89 SEASTAR_MODULE_EXPORT
90 template <typename T, size_t items_per_chunk = 128>
91 class chunked_fifo {
92  static_assert((items_per_chunk & (items_per_chunk - 1)) == 0,
93  "chunked_fifo chunk size must be power of two");
94  union maybe_item {
95  maybe_item() noexcept {}
96  ~maybe_item() {}
97  T data;
98  };
99  struct chunk {
100  maybe_item items[items_per_chunk];
101  struct chunk* next;
102  // begin and end interpreted mod items_per_chunk
103  unsigned begin;
104  unsigned end;
105  };
106  // We pop from the chunk at _front_chunk. This chunk is then linked to
107  // the following chunks via the "next" link. _back_chunk points to the
108  // last chunk in this list, and it is where we push.
109  chunk* _front_chunk = nullptr; // where we pop
110  chunk* _back_chunk = nullptr; // where we push
111  // We want an O(1) size but don't want to maintain a size() counter
112  // because this will slow down every push and pop operation just for
113  // the rare size() call. Instead, we just keep a count of chunks (which
114  // doesn't change on every push or pop), from which we can calculate
115  // size() when needed, and still be O(1).
116  // This assumes the invariant that all middle chunks (except the front
117  // and back) are always full.
118  size_t _nchunks = 0;
119  // A list of freed chunks, to support reserve() and to improve
120  // performance of repeated push and pop, especially on an empty queue.
121  // It is a performance/memory tradeoff how many freed chunks to keep
122  // here (see save_free_chunks constant below).
123  chunk* _free_chunks = nullptr;
124  size_t _nfree_chunks = 0;
125 public:
126  using value_type = T;
127  using size_type = size_t;
128  using reference = T&;
129  using pointer = T*;
130  using const_reference = const T&;
131  using const_pointer = const T*;
132 
133 private:
134  template <bool IsConst>
135  class basic_iterator {
136  friend class chunked_fifo;
137 
138  public:
139  using iterator_category = std::forward_iterator_tag;
140  using difference_type = std::ptrdiff_t;
141  using value_type = std::conditional_t<IsConst, const T, T>;
142  using pointer = value_type*;
143  using reference = value_type&;
144 
145  private:
146  using chunk_t = std::conditional_t<IsConst, const chunk, chunk>;
147  chunk_t* _chunk = nullptr;
148  size_t _item_index = 0;
149 
150  protected:
151  inline explicit basic_iterator(chunk_t* c) noexcept;
152  inline basic_iterator(chunk_t* c, size_t item_index) noexcept;
153 
154  public:
155  basic_iterator() noexcept = default;
156  template<bool OtherIsConst, std::enable_if_t<IsConst && !OtherIsConst, int> = 0>
157  inline basic_iterator(const basic_iterator<OtherIsConst>& o) noexcept
158  : basic_iterator{o._chunk, o._item_index} {}
159  inline bool operator==(const basic_iterator& o) const noexcept;
160  inline bool operator!=(const basic_iterator& o) const noexcept;
161  inline pointer operator->() const noexcept;
162  inline reference operator*() const noexcept;
163  inline basic_iterator operator++(int) noexcept;
164  basic_iterator& operator++() noexcept;
165  };
166 
167 public:
168  using iterator = basic_iterator<false>;
169  using const_iterator = basic_iterator<true>;
170 
171 public:
172  chunked_fifo() noexcept = default;
173  chunked_fifo(chunked_fifo&& x) noexcept;
174  chunked_fifo(const chunked_fifo& X) = delete;
175  ~chunked_fifo();
176  chunked_fifo& operator=(const chunked_fifo&) = delete;
177  chunked_fifo& operator=(chunked_fifo&&) noexcept;
178  inline void push_back(const T& data);
179  inline void push_back(T&& data);
180  T& back() noexcept;
181  const T& back() const noexcept;
182  template <typename... A>
183  inline void emplace_back(A&&... args);
184  inline T& front() const noexcept;
185  inline void pop_front() noexcept;
186  inline bool empty() const noexcept;
187  inline size_t size() const noexcept;
188  void clear() noexcept;
189  // reserve(n) ensures that at least (n - size()) further push() calls can
190  // be served without needing new memory allocation.
191  // Calling pop()s between these push()es is also allowed and does not
192  // alter this guarantee.
193  // Note that reserve() does not reduce the amount of memory already
194  // reserved - use shrink_to_fit() for that.
195  void reserve(size_t n);
196  // shrink_to_fit() frees memory held, but unused, by the queue. Such
197  // unused memory might exist after pops, or because of reserve().
198  void shrink_to_fit() noexcept;
199  inline iterator begin() noexcept;
200  inline iterator end() noexcept;
201  inline const_iterator begin() const noexcept;
202  inline const_iterator end() const noexcept;
203  inline const_iterator cbegin() const noexcept;
204  inline const_iterator cend() const noexcept;
205 private:
206  void back_chunk_new();
207  void front_chunk_delete() noexcept;
208  inline void ensure_room_back();
209  void undo_room_back() noexcept;
210  static inline size_t mask(size_t idx) noexcept;
211 
212 };
213 
214 template <typename T, size_t items_per_chunk>
215 template <bool IsConst>
216 inline
217 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::basic_iterator(chunk_t* c) noexcept : _chunk(c), _item_index(_chunk ? _chunk->begin : 0) {
218 }
219 
220 template <typename T, size_t items_per_chunk>
221 template <bool IsConst>
222 inline
223 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::basic_iterator(chunk_t* c, size_t item_index) noexcept : _chunk(c), _item_index(item_index) {
224 }
225 
226 template <typename T, size_t items_per_chunk>
227 template <bool IsConst>
228 inline bool
229 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::operator==(const basic_iterator& o) const noexcept {
230  return _chunk == o._chunk && _item_index == o._item_index;
231 }
232 
233 template <typename T, size_t items_per_chunk>
234 template <bool IsConst>
235 inline bool
236 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::operator!=(const basic_iterator& o) const noexcept {
237  return !(*this == o);
238 }
239 
240 template <typename T, size_t items_per_chunk>
241 template <bool IsConst>
242 inline typename chunked_fifo<T, items_per_chunk>::template basic_iterator<IsConst>::pointer
243 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::operator->() const noexcept {
244  return &_chunk->items[chunked_fifo::mask(_item_index)].data;
245 }
246 
247 template <typename T, size_t items_per_chunk>
248 template <bool IsConst>
249 inline typename chunked_fifo<T, items_per_chunk>::template basic_iterator<IsConst>::reference
250 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::operator*() const noexcept {
251  return _chunk->items[chunked_fifo::mask(_item_index)].data;
252 }
253 
254 template <typename T, size_t items_per_chunk>
255 template <bool IsConst>
256 inline typename chunked_fifo<T, items_per_chunk>::template basic_iterator<IsConst>
257 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::operator++(int) noexcept {
258  auto it = *this;
259  ++(*this);
260  return it;
261 }
262 
263 template <typename T, size_t items_per_chunk>
264 template <bool IsConst>
265 typename chunked_fifo<T, items_per_chunk>::template basic_iterator<IsConst>&
266 chunked_fifo<T, items_per_chunk>::basic_iterator<IsConst>::operator++() noexcept {
267  ++_item_index;
268  if (_item_index == _chunk->end) {
269  _chunk = _chunk->next;
270  _item_index = _chunk ? _chunk->begin : 0;
271  }
272  return *this;
273 }
274 
275 template <typename T, size_t items_per_chunk>
276 inline
277 chunked_fifo<T, items_per_chunk>::chunked_fifo(chunked_fifo&& x) noexcept
278  : _front_chunk(x._front_chunk)
279  , _back_chunk(x._back_chunk)
280  , _nchunks(x._nchunks)
281  , _free_chunks(x._free_chunks)
282  , _nfree_chunks(x._nfree_chunks) {
283  x._front_chunk = nullptr;
284  x._back_chunk = nullptr;
285  x._nchunks = 0;
286  x._free_chunks = nullptr;
287  x._nfree_chunks = 0;
288 }
289 
290 template <typename T, size_t items_per_chunk>
291 inline
292 chunked_fifo<T, items_per_chunk>&
293 chunked_fifo<T, items_per_chunk>::operator=(chunked_fifo&& x) noexcept {
294  if (&x != this) {
295  this->~chunked_fifo();
296  new (this) chunked_fifo(std::move(x));
297  }
298  return *this;
299 }
300 
301 template <typename T, size_t items_per_chunk>
302 inline size_t
303 chunked_fifo<T, items_per_chunk>::mask(size_t idx) noexcept {
304  return idx & (items_per_chunk - 1);
305 }
306 
307 template <typename T, size_t items_per_chunk>
308 inline bool
309 chunked_fifo<T, items_per_chunk>::empty() const noexcept {
310  return _front_chunk == nullptr;
311 }
312 
313 template <typename T, size_t items_per_chunk>
314 inline size_t
315 chunked_fifo<T, items_per_chunk>::size() const noexcept{
316  if (_front_chunk == nullptr) {
317  return 0;
318  } else if (_back_chunk == _front_chunk) {
319  // Single chunk.
320  return _front_chunk->end - _front_chunk->begin;
321  } else {
322  return _front_chunk->end - _front_chunk->begin
323  +_back_chunk->end - _back_chunk->begin
324  + (_nchunks - 2) * items_per_chunk;
325  }
326 }
327 
328 template <typename T, size_t items_per_chunk>
329 void chunked_fifo<T, items_per_chunk>::clear() noexcept {
330 #if 1
331  while (!empty()) {
332  pop_front();
333  }
334 #else
335  // This is specialized code to free the contents of all the chunks and the
336  // chunks themselves. but since destroying a very full queue is not an
337  // important use case to optimize, the simple loop above is preferable.
338  if (!_front_chunk) {
339  // Empty, nothing to do
340  return;
341  }
342  // Delete front chunk (partially filled)
343  for (auto i = _front_chunk->begin; i != _front_chunk->end; ++i) {
344  _front_chunk->items[mask(i)].data.~T();
345  }
346  chunk *p = _front_chunk->next;
347  delete _front_chunk;
348  // Delete all the middle chunks (all completely filled)
349  if (p) {
350  while (p != _back_chunk) {
351  // These are full chunks
352  chunk *nextp = p->next;
353  for (auto i = 0; i != items_per_chunk; ++i) {
354  // Note we delete out of order (we don't start with p->begin).
355  // That should be fine..
356  p->items[i].data.~T();
357  }
358  delete p;
359  p = nextp;
360  }
361  // Finally delete back chunk (partially filled)
362  for (auto i = _back_chunk->begin; i != _back_chunk->end; ++i) {
363  _back_chunk->items[mask(i)].data.~T();
364  }
365  delete _back_chunk;
366  }
367  _front_chunk = nullptr;
368  _back_chunk = nullptr;
369  _nchunks = 0;
370 #endif
371 }
372 
373 template <typename T, size_t items_per_chunk> void
374 chunked_fifo<T, items_per_chunk>::shrink_to_fit() noexcept {
375  while (_free_chunks) {
376  auto next = _free_chunks->next;
377  delete _free_chunks;
378  _free_chunks = next;
379  }
380  _nfree_chunks = 0;
381 }
382 
383 template <typename T, size_t items_per_chunk>
384 chunked_fifo<T, items_per_chunk>::~chunked_fifo() {
385  clear();
386  shrink_to_fit();
387 }
388 
389 template <typename T, size_t items_per_chunk>
390 void
391 chunked_fifo<T, items_per_chunk>::back_chunk_new() {
392  chunk *old = _back_chunk;
393  if (_free_chunks) {
394  _back_chunk = _free_chunks;
395  _free_chunks = _free_chunks->next;
396  --_nfree_chunks;
397  } else {
398  _back_chunk = new chunk;
399  }
400  _back_chunk->next = nullptr;
401  _back_chunk->begin = 0;
402  _back_chunk->end = 0;
403  if (old) {
404  old->next = _back_chunk;
405  }
406  if (_front_chunk == nullptr) {
407  _front_chunk = _back_chunk;
408  }
409  _nchunks++;
410 }
411 
412 
413 template <typename T, size_t items_per_chunk>
414 inline void
415 chunked_fifo<T, items_per_chunk>::ensure_room_back() {
416  // If we don't have a back chunk or it's full, we need to create a new one
417  if (_back_chunk == nullptr ||
418  (_back_chunk->end - _back_chunk->begin) == items_per_chunk) {
419  back_chunk_new();
420  }
421 }
422 
423 template <typename T, size_t items_per_chunk>
424 void
425 chunked_fifo<T, items_per_chunk>::undo_room_back() noexcept {
426  // If we failed creating a new item after ensure_room_back() created a
427  // new empty chunk, we must remove it, or empty() will be incorrect
428  // (either immediately, if the fifo was empty, or when all the items are
429  // popped, if it already had items).
430  if (_back_chunk->begin == _back_chunk->end) {
431  delete _back_chunk;
432  --_nchunks;
433  if (_nchunks == 0) {
434  _back_chunk = nullptr;
435  _front_chunk = nullptr;
436  } else {
437  // Because we don't usually pop from the back, we don't have a "prev"
438  // pointer so we need to find the previous chunk the hard and slow
439  // way. B
440  chunk *old = _back_chunk;
441  _back_chunk = _front_chunk;
442  while (_back_chunk->next != old) {
443  _back_chunk = _back_chunk->next;
444  }
445  _back_chunk->next = nullptr;
446  }
447  }
448 
449 }
450 
451 template <typename T, size_t items_per_chunk>
452 template <typename... Args>
453 inline void
454 chunked_fifo<T, items_per_chunk>::emplace_back(Args&&... args) {
455  ensure_room_back();
456  auto p = &_back_chunk->items[mask(_back_chunk->end)].data;
457  try {
458  new(p) T(std::forward<Args>(args)...);
459  } catch(...) {
460  undo_room_back();
461  throw;
462  }
463  ++_back_chunk->end;
464 }
465 
466 template <typename T, size_t items_per_chunk>
467 inline void
468 chunked_fifo<T, items_per_chunk>::push_back(const T& data) {
469  ensure_room_back();
470  auto p = &_back_chunk->items[mask(_back_chunk->end)].data;
471  try {
472  new(p) T(data);
473  } catch(...) {
474  undo_room_back();
475  throw;
476  }
477  ++_back_chunk->end;
478 }
479 
480 template <typename T, size_t items_per_chunk>
481 inline void
482 chunked_fifo<T, items_per_chunk>::push_back(T&& data) {
483  ensure_room_back();
484  auto p = &_back_chunk->items[mask(_back_chunk->end)].data;
485  try {
486  new(p) T(std::move(data));
487  } catch(...) {
488  undo_room_back();
489  throw;
490  }
491  ++_back_chunk->end;
492 }
493 
494 template <typename T, size_t items_per_chunk>
495 inline
496 T&
497 chunked_fifo<T, items_per_chunk>::back() noexcept {
498  return _back_chunk->items[mask(_back_chunk->end - 1)].data;
499 }
500 
501 template <typename T, size_t items_per_chunk>
502 inline
503 const T&
504 chunked_fifo<T, items_per_chunk>::back() const noexcept {
505  return _back_chunk->items[mask(_back_chunk->end - 1)].data;
506 }
507 
508 template <typename T, size_t items_per_chunk>
509 inline T&
510 chunked_fifo<T, items_per_chunk>::front() const noexcept {
511  return _front_chunk->items[mask(_front_chunk->begin)].data;
512 }
513 
514 template <typename T, size_t items_per_chunk>
515 inline void
516 chunked_fifo<T, items_per_chunk>::front_chunk_delete() noexcept {
517  chunk *next = _front_chunk->next;
518  // Certain use cases may need to repeatedly allocate and free a chunk -
519  // an obvious example is an empty queue to which we push, and then pop,
520  // repeatedly. Another example is pushing and popping to a non-empty queue
521  // we push and pop at different chunks so we need to free and allocate a
522  // chunk every items_per_chunk operations.
523  // The solution is to keep a list of freed chunks instead of freeing them
524  // immediately. There is a performance/memory tradeoff of how many freed
525  // chunks to save: If we save them all, the queue can never shrink from
526  // its maximum memory use (this is how circular_buffer behaves).
527  // The ad-hoc choice made here is to limit the number of saved chunks to 1,
528  // but this could easily be made a configuration option.
529  static constexpr int save_free_chunks = 1;
530  if (_nfree_chunks < save_free_chunks) {
531  _front_chunk->next = _free_chunks;
532  _free_chunks = _front_chunk;
533  ++_nfree_chunks;
534  } else {
535  delete _front_chunk;
536  }
537  // If we only had one chunk, _back_chunk is gone too.
538  if (_back_chunk == _front_chunk) {
539  _back_chunk = nullptr;
540  }
541  _front_chunk = next;
542  --_nchunks;
543 }
544 
545 template <typename T, size_t items_per_chunk>
546 inline void
547 chunked_fifo<T, items_per_chunk>::pop_front() noexcept {
548  front().~T();
549  // If the front chunk has become empty, we need to free remove it and use
550  // the next one.
551  if (++_front_chunk->begin == _front_chunk->end) {
552  front_chunk_delete();
553  }
554 }
555 
556 template <typename T, size_t items_per_chunk>
557 void chunked_fifo<T, items_per_chunk>::reserve(size_t n) {
558  // reserve() guarantees that (n - size()) additional push()es will
559  // succeed without reallocation:
560  if (n <= size()) {
561  return;
562  }
563  size_t need = n - size();
564  // If we already have a back chunk, it might have room for some pushes
565  // before filling up, so decrease "need":
566  if (_back_chunk) {
567  size_t back_chunk_n = items_per_chunk - (_back_chunk->end - _back_chunk->begin);
568  need -= std::min(back_chunk_n, need);
569  }
570  size_t needed_chunks = (need + items_per_chunk - 1) / items_per_chunk;
571  // If we already have some freed chunks saved, we need to allocate fewer
572  // additional chunks, or none at all
573  if (needed_chunks <= _nfree_chunks) {
574  return;
575  }
576  needed_chunks -= _nfree_chunks;
577  while (needed_chunks--) {
578  chunk *c = new chunk;
579  c->next = _free_chunks;
580  _free_chunks = c;
581  ++_nfree_chunks;
582  }
583 }
584 
585 template <typename T, size_t items_per_chunk>
586 inline typename chunked_fifo<T, items_per_chunk>::iterator
587 chunked_fifo<T, items_per_chunk>::begin() noexcept {
588  return iterator(_front_chunk);
589 }
590 
591 template <typename T, size_t items_per_chunk>
592 inline typename chunked_fifo<T, items_per_chunk>::iterator
593 chunked_fifo<T, items_per_chunk>::end() noexcept {
594  return iterator(nullptr);
595 }
596 
597 template <typename T, size_t items_per_chunk>
598 inline typename chunked_fifo<T, items_per_chunk>::const_iterator
599 chunked_fifo<T, items_per_chunk>::begin() const noexcept {
600  return const_iterator(_front_chunk);
601 }
602 
603 template <typename T, size_t items_per_chunk>
604 inline typename chunked_fifo<T, items_per_chunk>::const_iterator
605 chunked_fifo<T, items_per_chunk>::end() const noexcept {
606  return const_iterator(nullptr);
607 }
608 
609 template <typename T, size_t items_per_chunk>
610 inline typename chunked_fifo<T, items_per_chunk>::const_iterator
611 chunked_fifo<T, items_per_chunk>::cbegin() const noexcept {
612  return const_iterator(_front_chunk);
613 }
614 
615 template <typename T, size_t items_per_chunk>
616 inline typename chunked_fifo<T, items_per_chunk>::const_iterator
617 chunked_fifo<T, items_per_chunk>::cend() const noexcept {
618  return const_iterator(nullptr);
619 }
620 
621 }
Definition: chunked_fifo.hh:91
Seastar API namespace.
Definition: abort_on_ebadf.hh:26