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