“Is all that we see or seem
But a dream within a dream?”
–Edgar Allan Poe
In part one of this series I’ve discussed the mod operator and demonstrated that it is a flexible tool for solving circular problems. I’ve also explained that the mod operator, as implemented by most (system) programming languages, is not able to handle problems involving negative dividends. As a remedy, I’ve introduced an efficient alternative of the mod operator for divisors that are base-2 numbers.
Today, I want to have a look at some first circular use-cases. Again, we meet our good old friend, the ring buffer.
Sometimes, you need to store data and keep previous versions of that data, but the memory available is limited. In such situations, an obvious approach is to use a ring buffer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+---------------------------------+ 0 | First data set | +---------------------------------+ 1 | Second data set | +---------------------------------+ 2 | ... | +---------------------------------+ 3 | ... | +---------------------------------+ 4 | ... | +---------------------------------+ 5 | ... | +---------------------------------+ 6 | ... | +---------------------------------+ 7 | Eighth data set | +---------------------------------+ |
In this example, we have a ring buffer (again, pretend that the last and first element are joined) comprising eight (N = 8) fixed-size slots for storing information. Each slot contains a data set. We start by writing to slot 0, then slot 1 and so on; when the ring buffer is full (that is, slot 7 has been written to), we wrap around to slot 0 and overwrite what has been stored there previously, thus kicking out the oldest data set. By using this scheme, we not only have access to the most recent data set, but also to up to seven predecessors.
In addition to the actual ring buffer, we need a variable to keep track of the most-recently updated (“MRU”) slot number; that is, an index that points to the “newest” data set. Based on the current value of the MRU we can easily calculate which slot is to be updated upon the next write access:
1 2 3 |
mru = (mru + 1) mod N |
Updating the ring buffer is just one part of the story, reading another. For instance, to get the predecessor of the current data set we need to calculate the slot index based on the previous value of MRU. Again, by using the mod operator, this can be achieved easily:
1 2 3 |
i = (mru - 1) mod N |
Dumping the whole ring buffer in reverse chronological order is achieved with this simple loop:
1 2 3 4 5 |
for i in (0 .. N - 1) print buffer[(mru - i) mod N] end |
Note that for this to work, the mod operator has to be a mathematical modulo operator since the dividends may become negative. If you don’t have access to a true mathematical mod operator, you should make N a base-2 number and use the modulo replacement operation given in the previous installment (mod_base2).
Now let’s make our ring buffer example a bit more interesting. Let’s pretend our ring buffer must be stored in persistent, non-volatile memory (NVM), like flash. One problem with flash memory is that it wears out over time: the more often you update flash memory, the shorter the data retention time gets. (It’s like using a bucket with a crack to carry water from one place to another and the crack gets bigger with every lap you go.)
Therefore, storing the most-recently updated value in a single flash variable is not a good idea, since this memory location would wear-out quickly: every update to a ring buffer would require an update to the MRU variable, which means that the update load on the MRU variable is N times the update load on the individual slots.
A frequently used countermeasure is to have a “distributed MRU variable”, which is achieved by prefixing or suffixing every slot with a so-called “age counter” field.
The age counter starts at zero and is incremented with every write to a slot. So after three updates to an initially empty ring buffer, we would have this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+---------------------------------+-----+ 0 | First data set | 1 | +---------------------------------+-----+ 1 | Second data set | 2 | +---------------------------------+-----+ 2 | Third data set | 3 | +---------------------------------+-----+ 3 | | 0 | +---------------------------------+-----+ 4 | | 0 | +---------------------------------+-----+ 5 | | 0 | +---------------------------------+-----+ 6 | | 0 | +---------------------------------+-----+ 7 | | 0 | +---------------------------------+-----+ |
After ten updates, the ring buffer would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+---------------------------------+-----+ 0 | ... | 9 | +---------------------------------+-----+ 1 | ... | 10 | +---------------------------------+-----+ 2 | ... | 3 | +---------------------------------+-----+ 3 | ... | 4 | +---------------------------------+-----+ 4 | ... | 5 | +---------------------------------+-----+ 5 | ... | 6 | +---------------------------------+-----+ 6 | ... | 7 | +---------------------------------+-----+ 7 | ... | 8 | +---------------------------------+-----+ |
The slot containing the age counter with the highest value must be the MRU slot, slot 1 in our example. For obvious reasons, the value range of the age counter must be larger than the slot range of the ring buffer, but sooner or later, even the age counter will wrap around. After many writes to the ring buffer we might get this picture:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+---------------------------------+-----+ 0 | ... | 253 | +---------------------------------+-----+ 1 | ... | 254 | +---------------------------------+-----+ 2 | ... | 255 | +---------------------------------+-----+ 3 | ... | 0 | +---------------------------------+-----+ 4 | ... | 1 | +---------------------------------+-----+ 5 | ... | 2 | +---------------------------------+-----+ 6 | ... | 3 | +---------------------------------+-----+ 7 | ... | 252 | +---------------------------------+-----+ |
So which slot is the MRU slot now? The age counter is obviously an unsigned 8-bit type, since it wrapped around at value 255. Starting with 253 (the age counter value of slot 0) and taken modulo 256, all counter values increase by one up to 3. But the next value, 252, is not a successor of 3; it is too far ahead (or rather behind). This means that the slot containing age counter value 3 must be the most recently updated slot (which is 6).
The MRU is the basis for all ring buffer operations and it makes sense to maintain its value in a RAM variable. But how can one determine the MRU programmatically, for instance after a reset when all RAM content is lost? Here is a method to search for it:
1 2 3 4 5 6 |
for mru in (0..N-2) if (buffer[mru].counter + 1) mod 256 != (buffer[mru + 1].counter) break end |
It is a good exercise to convince yourself that this algorithm actually works. Think about the initial “empty” ring buffer case where all age counters are zero as well as the case where the “highest” age counter is stored in the last slot.
Luckily, the dividends in this algorithm cannot become negative, so if we wanted to code this in C, we could use the ‘%’ operator. But since we are doing a modulo 256 operation, we can apply the optimization given in the first installement, which is replacing the modulo 256 operation with a (uint8_t) cast:
1 2 3 4 5 6 7 |
int mru; for (mru = 0; mru <= N - 2; ++mru) { if ( (uint8_t) (buffer[mru].counter + 1) != (buffer[mru + 1].counter) ) break; } |
Once we are in possession of the MRU, we can work with this ring buffer just like we did with the previous ring buffer. The only thing we have to do in addition is to memorize the highest (or current) age counter value for future updates to the ring buffer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Determine current age counter. age_counter = buffer[mru].counter ... # Print most recent data set. print buffer.dataset[mru] # Print oldest data set. print buffer.dataset[(mru - N - 1) mod N] # Write a new data set. mru = (mru + 1) mod N age_counter = (age_counter + 1) mod 256 buffer[mru].age_counter = age_counter buffer[mru].dataset = "some new data" |
Finding the MRU in our second ring buffer was not particularly difficult — we just needed to compare the age counters until we found a “discontinuity”. We knew that the slots are updated, one after another, cyclically, with increasing age counter values. Things get more complicated when there is no such strict sequential ordering. Let me explain.
Imaginge you want to keep track of when certain events occur. For each kind of event, you would store a timestamp from a 32-bit timer source that indicates when this particular event occurred:
1 2 3 4 5 6 7 8 9 10 11 12 |
+---------------------------------+-----------+ 0 | First type of event | 353011 | +---------------------------------+-----------+ 1 | Second type of event | 1643 | +---------------------------------+-----------+ 2 | Third type of event | 4 | +---------------------------------+-----------+ 3 | ... | ... | +---------------------------------+-----------+ : : : |
Further, assume that the 32-bit timer can wrap around (otherwise we wouldn’t have a circular problem), just like our 8-bit age counter wrapped around in our ring buffer example. How would you know which event occurred first or last? How would you calculate the relative time between two events? This and more is the topic of the next part of this series. Stay tuned…