]> git.proxmox.com Git - ceph.git/blob - ceph/src/seastar/include/seastar/core/chunked_fifo.hh
update sources to ceph Nautilus 14.2.1
[ceph.git] / ceph / src / seastar / include / seastar / core / 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 #include <memory>
25 #include <algorithm>
26
27 namespace seastar {
28
29 // An unbounded FIFO queue of objects of type T.
30 //
31 // It provides operations to push items in one end of the queue, and pop them
32 // from the other end of the queue - both operations are guaranteed O(1)
33 // (not just amortized O(1)). The size() operation is also O(1).
34 // chunked_fifo also guarantees that the largest contiguous memory allocation
35 // it does is O(1). The total memory used is, of course, O(N).
36 //
37 // How does chunked_fifo differ from std::list<>, circular_buffer<> and
38 // std::deque()?
39 //
40 // std::list<> can also make all the above guarantees, but is inefficient -
41 // both at run speed (every operation requires an allocation), and in memory
42 // use. Much more efficient than std::list<> is our circular_buffer<>, which
43 // allocates a contiguous array to hold the items and only reallocates it,
44 // exponentially, when the queue grows. On one test of several different
45 // push/pop scenarios, circular_buffer<> was between 5 and 20 times faster
46 // than std::list, and also used considerably less memory.
47 // The problem with circular_buffer<> is that gives up on the last guarantee
48 // we made above: circular_buffer<> allocates all the items in one large
49 // contiguous allocation - that might not be possible when the memory is
50 // highly fragmented.
51 // std::deque<> aims to solve the contiguous allocation problem by allocating
52 // smaller chunks of the queue, and keeping a list of them in an array. This
53 // array is necessary to allow for O(1) random access to any element, a
54 // feature which we do not need; But this array is itself contiguous so
55 // std::deque<> attempts larger contiguous allocations the larger the queue
56 // gets: std::deque<>'s contiguous allocation is still O(N) and in fact
57 // exactly 1/64 of the size of circular_buffer<>'s contiguous allocation.
58 // So it's an improvement over circular_buffer<>, but not a full solution.
59 //
60 // chunked_fifo<> is such a solution: it also allocates the queue in fixed-
61 // size chunks (just like std::deque) but holds them in a linked list, not
62 // a contiguous array, so there are no large contiguous allocations.
63 //
64 // Unlike std::deque<> or circular_buffer<>, chunked_fifo only provides the
65 // operations needed by std::queue, i.e.,: empty(), size(), front(), back(),
66 // push_back() and pop_front(). For simplicity, we do *not* implement other
67 // possible operations, like inserting or deleting elements from the "wrong"
68 // side of the queue or from the middle, nor random-access to items in the
69 // middle of the queue or iteration over the items without popping them.
70 //
71 // Another feature of chunked_fifo which std::deque is missing is the ability
72 // to control the chunk size, as a template parameter. In std::deque the
73 // chunk size is undocumented and fixed - in gcc, it is always 512 bytes.
74 // chunked_fifo, on the other hand, makes the chunk size (in number of items
75 // instead of bytes) a template parameter; In situations where the queue is
76 // expected to become very long, using a larger chunk size might make sense
77 // because it will result in fewer allocations.
78 //
79 // chunked_fifo uses uninitialized storage for unoccupied elements, and thus
80 // uses move/copy constructors instead of move/copy assignments, which are
81 // less efficient.
82
83 template <typename T, size_t items_per_chunk = 128>
84 class chunked_fifo {
85 static_assert((items_per_chunk & (items_per_chunk - 1)) == 0,
86 "chunked_fifo chunk size must be power of two");
87 union maybe_item {
88 maybe_item() noexcept {}
89 ~maybe_item() {}
90 T data;
91 };
92 struct chunk {
93 maybe_item items[items_per_chunk];
94 struct chunk* next;
95 // begin and end interpreted mod items_per_chunk
96 unsigned begin;
97 unsigned end;
98 };
99 // We pop from the chunk at _front_chunk. This chunk is then linked to
100 // the following chunks via the "next" link. _back_chunk points to the
101 // last chunk in this list, and it is where we push.
102 chunk* _front_chunk = nullptr; // where we pop
103 chunk* _back_chunk = nullptr; // where we push
104 // We want an O(1) size but don't want to maintain a size() counter
105 // because this will slow down every push and pop operation just for
106 // the rare size() call. Instead, we just keep a count of chunks (which
107 // doesn't change on every push or pop), from which we can calculate
108 // size() when needed, and still be O(1).
109 // This assumes the invariant that all middle chunks (except the front
110 // and back) are always full.
111 size_t _nchunks = 0;
112 // A list of freed chunks, to support reserve() and to improve
113 // performance of repeated push and pop, especially on an empty queue.
114 // It is a performance/memory tradeoff how many freed chunks to keep
115 // here (see save_free_chunks constant below).
116 chunk* _free_chunks = nullptr;
117 size_t _nfree_chunks = 0;
118 public:
119 using value_type = T;
120 using size_type = size_t;
121 using reference = T&;
122 using pointer = T*;
123 using const_reference = const T&;
124 using const_pointer = const T*;
125 public:
126 chunked_fifo() = default;
127 chunked_fifo(chunked_fifo&& x) noexcept;
128 chunked_fifo(const chunked_fifo& X) = delete;
129 ~chunked_fifo();
130 chunked_fifo& operator=(const chunked_fifo&) = delete;
131 chunked_fifo& operator=(chunked_fifo&&) noexcept;
132 inline void push_back(const T& data);
133 inline void push_back(T&& data);
134 T& back();
135 const T& back() const;
136 template <typename... A>
137 inline void emplace_back(A&&... args);
138 inline T& front() const noexcept;
139 inline void pop_front() noexcept;
140 inline bool empty() const noexcept;
141 inline size_t size() const noexcept;
142 void clear() noexcept;
143 // reserve(n) ensures that at least (n - size()) further push() calls can
144 // be served without needing new memory allocation.
145 // Calling pop()s between these push()es is also allowed and does not
146 // alter this guarantee.
147 // Note that reserve() does not reduce the amount of memory already
148 // reserved - use shrink_to_fit() for that.
149 void reserve(size_t n);
150 // shrink_to_fit() frees memory held, but unused, by the queue. Such
151 // unused memory might exist after pops, or because of reserve().
152 void shrink_to_fit();
153 private:
154 void back_chunk_new();
155 void front_chunk_delete() noexcept;
156 inline void ensure_room_back();
157 void undo_room_back();
158 inline size_t mask(size_t idx) const noexcept;
159
160 };
161
162 template <typename T, size_t items_per_chunk>
163 inline
164 chunked_fifo<T, items_per_chunk>::chunked_fifo(chunked_fifo&& x) noexcept
165 : _front_chunk(x._front_chunk)
166 , _back_chunk(x._back_chunk)
167 , _nchunks(x._nchunks)
168 , _free_chunks(x._free_chunks)
169 , _nfree_chunks(x._nfree_chunks) {
170 x._front_chunk = nullptr;
171 x._back_chunk = nullptr;
172 x._nchunks = 0;
173 x._free_chunks = nullptr;
174 x._nfree_chunks = 0;
175 }
176
177 template <typename T, size_t items_per_chunk>
178 inline
179 chunked_fifo<T, items_per_chunk>&
180 chunked_fifo<T, items_per_chunk>::operator=(chunked_fifo&& x) noexcept {
181 if (&x != this) {
182 this->~chunked_fifo();
183 new (this) chunked_fifo(std::move(x));
184 }
185 return *this;
186 }
187
188 template <typename T, size_t items_per_chunk>
189 inline size_t
190 chunked_fifo<T, items_per_chunk>::mask(size_t idx) const noexcept {
191 return idx & (items_per_chunk - 1);
192 }
193
194 template <typename T, size_t items_per_chunk>
195 inline bool
196 chunked_fifo<T, items_per_chunk>::empty() const noexcept {
197 return _front_chunk == nullptr;
198 }
199
200 template <typename T, size_t items_per_chunk>
201 inline size_t
202 chunked_fifo<T, items_per_chunk>::size() const noexcept{
203 if (_front_chunk == nullptr) {
204 return 0;
205 } else if (_back_chunk == _front_chunk) {
206 // Single chunk.
207 return _front_chunk->end - _front_chunk->begin;
208 } else {
209 return _front_chunk->end - _front_chunk->begin
210 +_back_chunk->end - _back_chunk->begin
211 + (_nchunks - 2) * items_per_chunk;
212 }
213 }
214
215 template <typename T, size_t items_per_chunk>
216 void chunked_fifo<T, items_per_chunk>::clear() noexcept {
217 #if 1
218 while (!empty()) {
219 pop_front();
220 }
221 #else
222 // This is specialized code to free the contents of all the chunks and the
223 // chunks themselves. but since destroying a very full queue is not an
224 // important use case to optimize, the simple loop above is preferable.
225 if (!_front_chunk) {
226 // Empty, nothing to do
227 return;
228 }
229 // Delete front chunk (partially filled)
230 for (auto i = _front_chunk->begin; i != _front_chunk->end; ++i) {
231 _front_chunk->items[mask(i)].data.~T();
232 }
233 chunk *p = _front_chunk->next;
234 delete _front_chunk;
235 // Delete all the middle chunks (all completely filled)
236 if (p) {
237 while (p != _back_chunk) {
238 // These are full chunks
239 chunk *nextp = p->next;
240 for (auto i = 0; i != items_per_chunk; ++i) {
241 // Note we delete out of order (we don't start with p->begin).
242 // That should be fine..
243 p->items[i].data.~T();
244 }
245 delete p;
246 p = nextp;
247 }
248 // Finally delete back chunk (partially filled)
249 for (auto i = _back_chunk->begin; i != _back_chunk->end; ++i) {
250 _back_chunk->items[mask(i)].data.~T();
251 }
252 delete _back_chunk;
253 }
254 _front_chunk = nullptr;
255 _back_chunk = nullptr;
256 _nchunks = 0;
257 #endif
258 }
259
260 template <typename T, size_t items_per_chunk> void
261 chunked_fifo<T, items_per_chunk>::shrink_to_fit() {
262 while (_free_chunks) {
263 auto next = _free_chunks->next;
264 delete _free_chunks;
265 _free_chunks = next;
266 }
267 _nfree_chunks = 0;
268 }
269
270 template <typename T, size_t items_per_chunk>
271 chunked_fifo<T, items_per_chunk>::~chunked_fifo() {
272 clear();
273 shrink_to_fit();
274 }
275
276 template <typename T, size_t items_per_chunk>
277 void
278 chunked_fifo<T, items_per_chunk>::back_chunk_new() {
279 chunk *old = _back_chunk;
280 if (_free_chunks) {
281 _back_chunk = _free_chunks;
282 _free_chunks = _free_chunks->next;
283 --_nfree_chunks;
284 } else {
285 _back_chunk = new chunk;
286 }
287 _back_chunk->next = nullptr;
288 _back_chunk->begin = 0;
289 _back_chunk->end = 0;
290 if (old) {
291 old->next = _back_chunk;
292 }
293 if (_front_chunk == nullptr) {
294 _front_chunk = _back_chunk;
295 }
296 _nchunks++;
297 }
298
299
300 template <typename T, size_t items_per_chunk>
301 inline void
302 chunked_fifo<T, items_per_chunk>::ensure_room_back() {
303 // If we don't have a back chunk or it's full, we need to create a new one
304 if (_back_chunk == nullptr ||
305 (_back_chunk->end - _back_chunk->begin) == items_per_chunk) {
306 back_chunk_new();
307 }
308 }
309
310 template <typename T, size_t items_per_chunk>
311 void
312 chunked_fifo<T, items_per_chunk>::undo_room_back() {
313 // If we failed creating a new item after ensure_room_back() created a
314 // new empty chunk, we must remove it, or empty() will be incorrect
315 // (either immediately, if the fifo was empty, or when all the items are
316 // popped, if it already had items).
317 if (_back_chunk->begin == _back_chunk->end) {
318 delete _back_chunk;
319 --_nchunks;
320 if (_nchunks == 0) {
321 _back_chunk = nullptr;
322 _front_chunk = nullptr;
323 } else {
324 // Because we don't usually pop from the back, we don't have a "prev"
325 // pointer so we need to find the previous chunk the hard and slow
326 // way. B
327 chunk *old = _back_chunk;
328 _back_chunk = _front_chunk;
329 while (_back_chunk->next != old) {
330 _back_chunk = _back_chunk->next;
331 }
332 _back_chunk->next = nullptr;
333 }
334 }
335
336 }
337
338 template <typename T, size_t items_per_chunk>
339 template <typename... Args>
340 inline void
341 chunked_fifo<T, items_per_chunk>::emplace_back(Args&&... args) {
342 ensure_room_back();
343 auto p = &_back_chunk->items[mask(_back_chunk->end)].data;
344 try {
345 new(p) T(std::forward<Args>(args)...);
346 } catch(...) {
347 undo_room_back();
348 throw;
349 }
350 ++_back_chunk->end;
351 }
352
353 template <typename T, size_t items_per_chunk>
354 inline void
355 chunked_fifo<T, items_per_chunk>::push_back(const T& data) {
356 ensure_room_back();
357 auto p = &_back_chunk->items[mask(_back_chunk->end)].data;
358 try {
359 new(p) T(data);
360 } catch(...) {
361 undo_room_back();
362 throw;
363 }
364 ++_back_chunk->end;
365 }
366
367 template <typename T, size_t items_per_chunk>
368 inline void
369 chunked_fifo<T, items_per_chunk>::push_back(T&& data) {
370 ensure_room_back();
371 auto p = &_back_chunk->items[mask(_back_chunk->end)].data;
372 try {
373 new(p) T(std::move(data));
374 } catch(...) {
375 undo_room_back();
376 throw;
377 }
378 ++_back_chunk->end;
379 }
380
381 template <typename T, size_t items_per_chunk>
382 inline
383 T&
384 chunked_fifo<T, items_per_chunk>::back() {
385 return _back_chunk->items[mask(_back_chunk->end - 1)].data;
386 }
387
388 template <typename T, size_t items_per_chunk>
389 inline
390 const T&
391 chunked_fifo<T, items_per_chunk>::back() const {
392 return _back_chunk->items[mask(_back_chunk->end - 1)].data;
393 }
394
395 template <typename T, size_t items_per_chunk>
396 inline T&
397 chunked_fifo<T, items_per_chunk>::front() const noexcept {
398 return _front_chunk->items[mask(_front_chunk->begin)].data;
399 }
400
401 template <typename T, size_t items_per_chunk>
402 inline void
403 chunked_fifo<T, items_per_chunk>::front_chunk_delete() noexcept {
404 chunk *next = _front_chunk->next;
405 // Certain use cases may need to repeatedly allocate and free a chunk -
406 // an obvious example is an empty queue to which we push, and then pop,
407 // repeatedly. Another example is pushing and popping to a non-empty queue
408 // we push and pop at different chunks so we need to free and allocate a
409 // chunk every items_per_chunk operations.
410 // The solution is to keep a list of freed chunks instead of freeing them
411 // immediately. There is a performance/memory tradeoff of how many freed
412 // chunks to save: If we save them all, the queue can never shrink from
413 // its maximum memory use (this is how circular_buffer behaves).
414 // The ad-hoc choice made here is to limit the number of saved chunks to 1,
415 // but this could easily be made a configuration option.
416 static constexpr int save_free_chunks = 1;
417 if (_nfree_chunks < save_free_chunks) {
418 _front_chunk->next = _free_chunks;
419 _free_chunks = _front_chunk;
420 ++_nfree_chunks;
421 } else {
422 delete _front_chunk;
423 }
424 // If we only had one chunk, _back_chunk is gone too.
425 if (_back_chunk == _front_chunk) {
426 _back_chunk = nullptr;
427 }
428 _front_chunk = next;
429 --_nchunks;
430 }
431
432 template <typename T, size_t items_per_chunk>
433 inline void
434 chunked_fifo<T, items_per_chunk>::pop_front() noexcept {
435 front().~T();
436 // If the front chunk has become empty, we need to free remove it and use
437 // the next one.
438 if (++_front_chunk->begin == _front_chunk->end) {
439 front_chunk_delete();
440 }
441 }
442
443 template <typename T, size_t items_per_chunk>
444 void chunked_fifo<T, items_per_chunk>::reserve(size_t n) {
445 // reserve() guarantees that (n - size()) additional push()es will
446 // succeed without reallocation:
447 size_t need = n - size();
448 // If we already have a back chunk, it might have room for some pushes
449 // before filling up, so decrease "need":
450 if (_back_chunk) {
451 need -= items_per_chunk - (_back_chunk->end - _back_chunk->begin);
452 }
453 size_t needed_chunks = (need + items_per_chunk - 1) / items_per_chunk;
454 // If we already have some freed chunks saved, we need to allocate fewer
455 // additional chunks, or none at all
456 if (needed_chunks <= _nfree_chunks) {
457 return;
458 }
459 needed_chunks -= _nfree_chunks;
460 while (needed_chunks--) {
461 chunk *c = new chunk;
462 c->next = _free_chunks;
463 _free_chunks = c;
464 ++_nfree_chunks;
465 }
466 }
467
468 }