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