mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
circularBuffer.hpp
Go to the documentation of this file.
1/** \file circularBuffer.hpp
2 * \author Jared R. Males
3 * \brief A circular buffer class
4 * \ingroup signal_processing_files
5 *
6 */
7
8//***********************************************************************//
9// Copyright 2020 Jared R. Males (jaredmales@gmail.com)
10//
11// This file is part of mxlib.
12//
13// mxlib is free software: you can redistribute it and/or modify
14// it under the terms of the GNU General Public License as published by
15// the Free Software Foundation, either version 3 of the License, or
16// (at your option) any later version.
17//
18// mxlib is distributed in the hope that it will be useful,
19// but WITHOUT ANY WARRANTY; without even the implied warranty of
20// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21// GNU General Public License for more details.
22//
23// You should have received a copy of the GNU General Public License
24// along with mxlib. If not, see <http://www.gnu.org/licenses/>.
25//***********************************************************************//
26
27#ifndef sigproc_circularBuffer
28#define sigproc_circularBuffer
29
30#include <vector>
31
32namespace mx
33{
34namespace sigproc
35{
36
37/** \defgroup circular_buffer Circular Buffer
38 * \brief A circular buffer class
39 *
40 * Three options for the circular buffer are provided, each inheriting the same underlying interface. The choices
41 * vary the way in which the wrapping is handled at the beginning and end of the storage. These are:
42 * - circularBufferMod: uses the mod operator, this is slowest in all situations and is provided for comparison
43 * - circularBufferBranch: uses an if statement (branch) to test for location in memory. Slower in sequential access,
44 * possibly slightly faster in random access.
45 * - circularBufferIndex: uses a pre-populated index array, which adds some memory overhead. Fastest in sequential
46 * access, possibly slightly slower in random access.
47 *
48 * Benchmarks are clear that circularBufferIndex is fastest for sequential acceess, e.g. one element after the other in
49 * sequential order, by something like 30%. For random access circularBufferBranch is possibly slightly faster, but not
50 * enough tests were performed to be conclusive.
51 *
52 * circularBufferMod is always much slower due to use of the `%` operator. Don't use this for real work.
53 *
54 * The memory overhead of circularBufferIndex is `3*maxEntries*sizeof(indexT)`, where `maxEntries` is the maximum length
55 * of the buffer, and indexT is the type used for indexing and sizes. The factor of 3 enables negative offsets, i.e.
56 * for traversal in reverse.
57 *
58 * \note circularBufferIndex will not properly wrap until it is full. Attempts to access wrapped entries prior
59 * to inserting maxEntries entries will segfault.
60 *
61 * \todo perform circular buffer testing on an isolated core, with one test per executable
62 * \todo implement a circular buffer with fixed power-of-two size to test `&` modulo
63 *
64 * \ingroup signal_processing
65 */
66
67/// CRTP base class for all circular buffers, providing the underlying memory management and accessors.
68/** The CRTP derived classes implement a standard interface based on how they handle wrapping from the end
69 * to the beginning of the buffer.
70 *
71 * \tparam _derivedT the child class type
72 * \tparam _storedT the type stored in teh circular buffer
73 * \tparam _indexT the index type, also used for sizes (must be signed)
74 *
75 * \ingroup circular_buffer
76 */
77template <typename _derivedT, typename _storedT, typename _indexT>
79{
80 public:
81 typedef _derivedT derivedT; ///< The child class
82
83 typedef _storedT storedT; ///< The type stored in the circular buffer
84 typedef _indexT indexT; ///< The index type, also used for sizes
85
86 static_assert(std::is_signed_v<_indexT> == true, "circularBuffer indexT must be signed");
87
88 protected:
89 std::vector<storedT> m_buffer; ///< The circular buffer storage
90
91 indexT m_maxEntries{ 0 }; ///< The maximum number of entries to allow in the buffer before wrapping
92 indexT m_nextEntry{ 0 }; ///< Index into m_buffer of the next entry. This is the oldest entry in the buffer.
93 indexT m_latest{ 0 }; ///< Index into m_buff of the latest entry. This is the newest entry in the buffer.
94
95 uint64_t m_mono{ 0 }; /**< A monotonic counter, which is incremented for each new entry,
96 and reset on call to maxEntries.*/
97
98 public:
99 /// Default c'tor
101
102 /// Sizing constructor
103 /** Sets the maximum size of the buffer. Note that this will not be the size until
104 * a full set of entries have been added to the buffer.
105 */
106 explicit circularBufferBase( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ );
107
108 /// Set the maximum size of the buffer.
109 /** Note that this will not be the size until
110 * a full set of entries have been added to the buffer.
111 */
112 void maxEntries( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ );
113
114 /// Get the maximum size of the buffer.
115 /** Note that this is not the current size of the buffer until
116 * at least maxEntries have been added. Use size() to check current size.
117 *
118 * \returns the maximum size of the buffer, m_maxEntries
119 */
121
122 /// Get the number of entries.
123 /** Note that this is the current size. It will not
124 * be the same as maxEntries() until at least maxEntries
125 * have been added.
126 */
127 indexT size() const;
128
129 /// Add the next entry to the circular buffer
130 /** Adds the entry (will incur a deep copy) and updates
131 * the wrapping system.
132 */
133 void nextEntry( const storedT &newEnt /**< [in] the new entry to add to the buffer*/ );
134
135 /// Move to the next entry in the circular buffer
136 /** If not yet at maxEntries entries, this emplaces a new default constructed entry.
137 * Otherwise it increments counters so that the old latest entry is now the newest entry.
138 */
139 void nextEntry();
140
141 /// Returns the index of the earliest entry
142 /** This is particularly useful for accessing entries with the at() function
143 * over an unchanging sequence if asynchronous updates are possible.
144 */
146
147 /// Returns the index of the latest entry
148 /**
149 */
151
152 uint64_t mono();
153
154 /// Get the entry at a given index
155 /** `idx=0` is the earliest entry in the buffer. `idx=1` is the one after that.
156 * I.e., this counts forward from the oldest data
157 *
158 * \returns a reference to the indicated entry in the circular buffer.
159 */
160 storedT &operator[]( indexT idx /**< [in] the index of the entry to access*/ );
161
162 const storedT &operator[]( indexT idx /**< [in] the index of the entry to access*/ ) const;
163
164 /// Get the entry at a given index relative a fixed reference entry
165 /** `idx=0` is the entry at refEntry. `idx=1` is the one after that.
166 * I.e., this counts forward from the oldest data
167 *
168 * \returns a reference to the indicated entry in the circular buffer.
169 */
170 storedT &at( indexT refEntry, ///< [in] the entry to start counting from
171 indexT idx ///< [in] the index of the entry to access
172 );
173
174 /// Get the entry at a given index relative a fixed start entry, const version
175 /** `idx=0` is the entry at refEntry. `idx=1` is the one after that.
176 * I.e., this counts forward from the oldest data
177 *
178 * \overload
179 *
180 * \returns a const reference to the indicated entry in the circular buffer.
181 */
182 const storedT &at( indexT refEntry, ///< [in] the entry to start counting from
183 indexT idx ///< [in] the index of the entry to access
184 ) const;
185
186 private:
187 derivedT &derived()
188 {
189 return *static_cast<derivedT *>( this );
190 }
191
192 const derivedT &derived() const
193 {
194 return *static_cast<const derivedT *>( this );
195 }
196};
197
198template <class derivedT, typename storedT, typename indexT>
202
203template <class derivedT, typename storedT, typename indexT>
208
209template <class derivedT, typename storedT, typename indexT>
211{
212 m_buffer.clear();
213 m_nextEntry = 0;
214 m_latest = 0;
215
216 m_maxEntries = maxEnt;
217 m_buffer.reserve( m_maxEntries );
218
219 derived().setMaxEntries( maxEnt );
220
221 m_mono = 0;
222}
223
224template <class derivedT, typename storedT, typename indexT>
229
230template <class derivedT, typename storedT, typename indexT>
232{
233 return m_buffer.size();
234}
235
236template <class derivedT, typename storedT, typename indexT>
238{
239 if( m_buffer.size() < m_maxEntries )
240 {
241 m_buffer.push_back( newEnt );
242 m_latest = m_buffer.size() - 1;
243 m_nextEntry = 0;
244 }
245 else
246 {
247 m_buffer[m_nextEntry] = newEnt;
248 m_latest = m_nextEntry;
249 ++m_nextEntry;
250 if( m_nextEntry > m_buffer.size() - 1 )
251 {
252 m_nextEntry = 0;
253 }
254 }
255
256 ++m_mono;
257}
258
259template <class derivedT, typename storedT, typename indexT>
261{
262 if( m_buffer.size() < m_maxEntries )
263 {
264 m_buffer.emplace_back();
265 m_latest = m_buffer.size() - 1;
266 m_nextEntry = 0;
267 }
268 else
269 {
270 m_latest = m_nextEntry;
271 ++m_nextEntry;
272 if( m_nextEntry > m_buffer.size() - 1 )
273 {
274 m_nextEntry = 0;
275 }
276 }
277
278 ++m_mono;
279}
280
281template <class derivedT, typename storedT, typename indexT>
286
287template <class derivedT, typename storedT, typename indexT>
292
293template <class derivedT, typename storedT, typename indexT>
295{
296 return m_mono;
297}
298
299template <class derivedT, typename storedT, typename indexT>
301{
302 return derived().at( m_nextEntry, idx );
303}
304
305template <class derivedT, typename storedT, typename indexT>
307{
308 return derived().at( m_nextEntry, idx );
309}
310
311template <class derivedT, typename storedT, typename indexT>
313{
314 return derived().at( refEntry, idx );
315}
316
317template <class derivedT, typename storedT, typename indexT>
319{
320 return derived().at( refEntry, idx );
321}
322
323/// Circular buffer which wraps with an if statement (branching) [faster than mod, less memory than index]
324/**
325 * \ingroup circular_buffer
326 */
327template <typename _storedT, typename _indexT>
328class circularBufferBranch : public circularBufferBase<circularBufferBranch<_storedT, _indexT>, _storedT, _indexT>
329{
330 public:
331 typedef _storedT storedT; ///< The maximum number of entries to allow in the buffer before wrapping
332 typedef _indexT indexT; ///< The index type, also used for sizes
333
334 public:
335 /// Default c'tor
339
340 /// Sizing constructor
341 /** Sets the maximum size of the buffer. Note that this will not be the size until
342 * a full set of entries have been added to the buffer.
343 */
344 explicit circularBufferBranch( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ )
346 {
347 }
348
349 /// Interface implementation for maxEntries.
350 /** Resets the wrap entry to 0.
351 */
352 void setMaxEntries( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ )
353 {
354 }
355
356 /// Interface implementation for entry access
357 /** Accesses the idx-th element relative to refEntry, using a branch (if-statement) to wrap
358 *
359 * \returns a reference to the idx-th element
360 */
361 storedT &at( indexT refEntry, ///< [in] the entry to start counting from
362 indexT idx ///< [in] the index of the entry to access
363 )
364 {
365 indexT _idx = refEntry + idx;
366 if( _idx < 0)
367 {
368 return this->m_buffer[_idx + this->m_buffer.size()];
369 }
370 else if( _idx > this->m_buffer.size()-1 )
371 {
372 return this->m_buffer[_idx - this->m_buffer.size()];
373 }
374 else
375 {
376 return this->m_buffer[_idx];
377 }
378 }
379
380 /// Interface implementation for entry access, const version
381 /** Accesses the idx-th element relative to refEntry, using a branch (if-statement) to wrap
382 *
383 * \overload
384 *
385 * \returns a const reference to the idx-th element
386 */
387 const storedT &at( indexT refEntry, ///< [in] the entry to start counting from
388 indexT idx ///< [in] the index of the entry to access
389 ) const
390 {
391 indexT _idx = refEntry + idx;
392 if( _idx < 0)
393 {
394 return this->m_buffer[_idx + this->m_buffer.size()];
395 }
396 else if( _idx > this->m_buffer.size()-1 )
397 {
398 return this->m_buffer[_idx - this->m_buffer.size()];
399 }
400 else
401 {
402 return this->m_buffer[_idx];
403 }
404 }
405};
406
407/// Circular buffer which wraps with the mod opoerator [very slow]
408/**
409 * \ingroup circular_buffer
410 */
411template <typename _storedT, typename _indexT>
412class circularBufferMod : public circularBufferBase<circularBufferMod<_storedT, _indexT>, _storedT, _indexT>
413{
414 public:
415 typedef _storedT storedT; ///< The maximum number of entries to allow in the buffer before wrapping
416 typedef _indexT indexT; ///< The index type, also used for sizes
417
418 /// Default c'tor
422
423 /// Sizing constructor
424 /** Sets the maximum size of the buffer. Note that this will not be the size until
425 * a full set of entries have been added to the buffer.
426 */
427 explicit circularBufferMod( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ )
429 {
430 }
431
432 /// Interface implementation for maxEntries.
433 /** A no-op
434 */
435 void setMaxEntries( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ )
436 {
437 }
438
439 /// Interface implementation for entry access
440 /** Accesses the idx-th element relative to refEntry, using the mod operator to wrap
441 *
442 * \returns a reference to the idx-th element
443 */
444 storedT &at( indexT refEntry, ///< [in] the entry to start counting from
445 indexT idx ///< [in] the index of the entry to access
446 )
447 {
448 return this->m_buffer[( refEntry + idx ) % this->m_buffer.size()];
449 }
450
451 /// Interface implementation for entry access, const version
452 /** Accesses the idx-th element relative to refEntry, using the mod operator to wrap
453 *
454 * \overload
455 *
456 * \returns a const reference to the idx-th element
457 */
458 const storedT &at( indexT refEntry, ///< [in] the entry to start counting from
459 indexT idx ///< [in] the index of the entry to access
460 ) const
461 {
462 return this->m_buffer[( refEntry + idx ) % this->m_buffer.size()];
463 }
464};
465
466/// Circular buffer which wraps with a pre-populated indices array [generally fastest]
467/**
468 * \ingroup circular_buffer
469 */
470template <typename _storedT, typename _indexT>
471class circularBufferIndex : public circularBufferBase<circularBufferIndex<_storedT, _indexT>, _storedT, _indexT>
472{
473 public:
474 typedef _storedT storedT; ///< The maximum number of entries to allow in the buffer before wrapping
475 typedef _indexT indexT; ///< The index type, also used for sizes
476
477 protected:
478 std::vector<size_t> m_indices; ///< Vector of indices for fast indexing into parent's m_buffer
479
480 public:
481 /// Default c'tor
485
486 /// Sizing constructor
487 /** Sets the maximum size of the buffer. Note that this will not be the size until
488 * a full set of entries have been added to the buffer.
489 */
490 explicit circularBufferIndex( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ )
492 {
493 }
494
495 /// Interface implementation for maxEntries.
496 /** Resizes and populates the indices array.
497 */
498 void setMaxEntries( indexT maxEnt /**< [in] the maximum number of entries this buffer will hold*/ )
499 {
500 m_indices.resize( 3 * this->m_maxEntries );
501 for( size_t i = 0; i < this->m_maxEntries; ++i )
502 {
503 m_indices[i] = i;
504 m_indices[this->m_maxEntries + i] = i;
505 m_indices[2*this->m_maxEntries + i] = i;
506 }
507 }
508
509 /// Interface implementation for entry access
510 /** Accesses the idx-th element relative to refEntry, using the pre-populated indices to wrap
511 *
512 * \returns a reference to the idx-th element
513 */
514 storedT &at( indexT refEntry, ///< [in] the entry to start counting from
515 indexT idx ///< [in] the index of the entry to access
516 )
517 {
518 return this->m_buffer[m_indices[this->m_maxEntries+refEntry + idx]];
519 }
520
521 /// Interface implementation for entry access, const version
522 /** Accesses the idx-th element relative to refEntry, using the pre-populated indices to wrap
523 *
524 * \overload
525 *
526 * \returns a const reference to the idx-th element
527 */
528 const storedT &at( indexT refEntry, ///< [in] the entry to start counting from
529 indexT idx ///< [in] the index of the entry to access
530 ) const
531 {
532 return this->m_buffer[this->m_maxEntries+m_indices[refEntry + idx]];
533 }
534};
535
536} // namespace sigproc
537} // namespace mx
538
539#endif // sigproc_circularBuffer
CRTP base class for all circular buffers, providing the underlying memory management and accessors.
storedT & at(indexT refEntry, indexT idx)
Get the entry at a given index relative a fixed reference entry.
storedT & operator[](indexT idx)
Get the entry at a given index.
circularBufferBase(indexT maxEnt)
Sizing constructor.
indexT maxEntries()
Get the maximum size of the buffer.
std::vector< storedT > m_buffer
The circular buffer storage.
void maxEntries(indexT maxEnt)
Set the maximum size of the buffer.
indexT size() const
Get the number of entries.
const storedT & at(indexT refEntry, indexT idx) const
Get the entry at a given index relative a fixed start entry, const version.
indexT earliest()
Returns the index of the earliest entry.
indexT latest()
Returns the index of the latest entry.
const storedT & operator[](indexT idx) const
indexT m_maxEntries
The maximum number of entries to allow in the buffer before wrapping.
_derivedT derivedT
The child class.
_storedT storedT
The type stored in the circular buffer.
_indexT indexT
The index type, also used for sizes.
void nextEntry()
Move to the next entry in the circular buffer.
indexT m_latest
Index into m_buff of the latest entry. This is the newest entry in the buffer.
indexT m_nextEntry
Index into m_buffer of the next entry. This is the oldest entry in the buffer.
void nextEntry(const storedT &newEnt)
Add the next entry to the circular buffer.
Circular buffer which wraps with an if statement (branching) [faster than mod, less memory than index...
_storedT storedT
The maximum number of entries to allow in the buffer before wrapping.
void setMaxEntries(indexT maxEnt)
Interface implementation for maxEntries.
const storedT & at(indexT refEntry, indexT idx) const
Interface implementation for entry access, const version.
storedT & at(indexT refEntry, indexT idx)
Interface implementation for entry access.
circularBufferBranch(indexT maxEnt)
Sizing constructor.
_indexT indexT
The index type, also used for sizes.
Circular buffer which wraps with a pre-populated indices array [generally fastest].
std::vector< size_t > m_indices
Vector of indices for fast indexing into parent's m_buffer.
const storedT & at(indexT refEntry, indexT idx) const
Interface implementation for entry access, const version.
_indexT indexT
The index type, also used for sizes.
circularBufferIndex(indexT maxEnt)
Sizing constructor.
_storedT storedT
The maximum number of entries to allow in the buffer before wrapping.
storedT & at(indexT refEntry, indexT idx)
Interface implementation for entry access.
void setMaxEntries(indexT maxEnt)
Interface implementation for maxEntries.
Circular buffer which wraps with the mod opoerator [very slow].
_storedT storedT
The maximum number of entries to allow in the buffer before wrapping.
_indexT indexT
The index type, also used for sizes.
const storedT & at(indexT refEntry, indexT idx) const
Interface implementation for entry access, const version.
circularBufferMod(indexT maxEnt)
Sizing constructor.
storedT & at(indexT refEntry, indexT idx)
Interface implementation for entry access.
void setMaxEntries(indexT maxEnt)
Interface implementation for maxEntries.
The mxlib c++ namespace.
Definition mxError.hpp:40