as some might have noticed I'm trying to implement a ring buffer. I want to have a certain amount of safety measures in the data structure while not losing too much efficiency.
My current solution implements two types of counters, an index, which is a direct offset into the buffers memory and a sequence number, which is just a counter of type size_t
. The sequence number is used by iterators to access the ring buffer. Therefore the ring buffer has to convert from sequence number to buffer index on every buffer access. This is usually rather efficient:
size_t offset = seqNum - m_tailSeq;
size_t index = (m_tailIdx + offset) % m_size;
where seqNum
is the sequence number to be converted, m_tailSeq
is the sequence number of the oldest element in the buffer, m_tailIdx
is the buffer index of the oldest element in the buffer and m_size
is the size of the buffer memory.
However, if I keep adding elements to the buffer long enough, the sequence numbers will overflow. So I have to check for this. And when I do that my short and sweet conversion turns into this monster:
size_type getIndex(size_type seqNum) const
{
size_type headSeq = m_tailSeq + m_numElements;
// sequence does not wrap around
if (m_tailSeq < headSeq)
{
// bounds check
if(m_tailSeq <= seqNum && seqNum < headSeq) {
size_type offset = seqNum - m_tailSeq;
return (m_tailIdx + offset) % m_size;
} else {
throw BaseException("RingBuffer: access out of bounds", __FILE__, __LINE__);
}
}
// sequence does wrap around
else if (headSeq < m_tailSeq)
{
//bounds check (inverted from above)
if(seqNum < headSeq) {
size_type offset = (SIZE_TYPE_MAX - m_tailSeq) + seqNum;
return (m_tailIdx + offset) % m_size;
} else if (seqNum >= m_tailSeq) {
size_type offset = seqNum - m_tailSeq;
return (m_tailIdx + offset) % m_size;
} else {
throw BaseException("RingBuffer: access out of bounds", __FILE__, __LINE__);
}
}
else if (isEmpty()) {
throw BaseException("RingBufferIterator: accessing empty buffer", __FILE__, __LINE__);
}
}
This amounts to two integer additions, one integer subtraction, three integer comparisons, and one modulo operation in the best case on ever single buffer access. Needless to say that iterating over the buffer becomes pretty expensive. However, since I want to use this buffer in high performance scenarios (i.e. event queue in soft real time application) I would like this data structure to be as efficient as possible.
The current use case would be as an event buffer. One (or possibly more than one) system would write events into the buffer and other systems (more than one) would process these events at their own pace without removing them. When the buffer is full old events are simply overwritten. This way I always have a record of the last few hundred events and different systems can go over them at their respective update rates and pick out the events that are relevant for them. The different systems will keep an iterator that points into the ring buffer so they know where they left off last time and where to resume. When a system starts processing events it needs to determine whether its iterator is still valid or whether it has been overwritten. Events are likely to be processed in big chunks at a time, so incrementing and dereferencing should be quick. So basically we're looking at an MPMC ring buffer in a potentially multithreaded context.
The only solution I can come up with myself is to move the burden of error checking to the user of the buffer. I.e. the user has to first check (by some means) if its iterator into the buffer is valid, make sure a certain stretch of the buffer stays valid and then iterate over this stretch without any further checks. However, this seems error prone since I have to check the safety of the access in multiple parts of the program instead of just one place and it will get hairy if I should ever decide to make the buffer thread safe.
Am I missing something? Can this be done any better? Am I committing some beginners mistake?