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