The Book of Gehn

Ouroboros - Circular Buffer

September 16, 2018

struct circular_buffer is a simple but quite efficient implementation of a circular buffer over a continuous and finite memory slice.

The name Ouroboros comes from the Ancient Greek and symbolize a snake eating its own tail – a convenient image for a circular buffer.

Source code can be found in the tiburoncin project. Enjoy it!

Playing with a Circular Buffer

You can run this code with cling and byexample

First, let’s load this module to play with it:

?: .L circular_buffer.c
?: #include "circular_buffer.h"
?: #include <string.h>

Now, let’s create a buffer of 16 bytes:

?: struct circular_buffer_t buf;
?: circular_buffer_init(&buf, 16);

The circular buffer has 2 pointers, the head that mark the end of the data and the begin of the free space and the tail, which it is the opposite of the head.

At the begin, both pointers point to the same place, the free space has the size of the whole buffer and the ready space is empty:

?: (buf.head == buf.tail)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 16

?: circular_buffer_get_ready(&buf)
(unsigned long) 0

When more data is written in the buffer, the head pointer moves forward, the free space is reduced and the ready space is increased, all of them in the same proportion.

The implementation does not track how many bytes are written. It is up to the caller to write in the buffer from the buffer’s head and notify the circular buffer how many bytes wrote.

For example, if we write 10 bytes:

?: memcpy(&buf.buf[buf.head], "AABBCCDDEE", 10);

Then we must notify how many bytes were written updating the head pointer:

?: circular_buffer_advance_head(&buf, 10);

?: (buf.head > buf.tail)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 6

?: circular_buffer_get_ready(&buf)
(unsigned long) 10

We can keep writing and reading from the buffer.

As we did with the write, once we read the data from the buffer’s tail we need to notify to the circular buffer that the data can be discarded.

?: memcpy(&buf.buf[buf.head], "FFGG", 4);
?: circular_buffer_advance_head(&buf, 4);

?: char read[8];
?: memcpy(read, &buf.buf[buf.tail], 8);
?: circular_buffer_advance_tail(&buf, 8);

?: read
(char [8]) "AABBCCDD"

How many bytes we can write is determined by how many free space the buffer has; how many bytes we can read is determined by how many ready space the buffer has.

It is up to the caller honor this.

?: (buf.head > buf.tail)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 2

?: circular_buffer_get_ready(&buf)
(unsigned long) 6

When the head pointer reaches the end of the buffer, the pointer is restarted and set back to the begin.

The free space is expanded from the head to the tail and the tail is in front of the head:

?: circular_buffer_advance_head(&buf, 2);

?: (buf.tail > buf.head)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 8

?: circular_buffer_get_ready(&buf)
(unsigned long) 8

Contiguous reading and writing

If we keep writing (moving the head), the ready space will not increase because the circular buffer will always inform how many contiguous bytes are ready (for reading) or free (for writing).

The ready space is limited by the end of the buffer in this case:

?: circular_buffer_advance_head(&buf, 2);

?: (buf.tail > buf.head)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 6

?: circular_buffer_get_ready(&buf)
(unsigned long) 8

When the head bites the tail

The critical point happens when the head reaches the tail.

Now, this is exactly the same situation that happen at the begin, when the buffer was empty, but now it means that it is full.

To differentiate these two cases, internally there is a flag that tracks when the head is behind the tail:

?: circular_buffer_advance_head(&buf, 6);

?: (buf.tail >= buf.head)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 0

?: circular_buffer_get_ready(&buf)
(unsigned long) 8

?: (buf.hbehind)
(bool) true

If we move the tail to the end of the buffer, the rest of the bytes written are ready to be consumed and the head is in front of the tail again:

?: circular_buffer_advance_tail(&buf, 8);

?: (buf.head >= buf.tail)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 8

?: circular_buffer_get_ready(&buf)
(unsigned long) 8

?: (buf.hbehind)
(bool) false

If we move the head to the end we will reach again to the ambiguous cases but the hbehind variable tell us that the head is behind the tail again:

?: circular_buffer_advance_head(&buf, 8);

?: (buf.tail >= buf.head)
(bool) true

?: circular_buffer_get_free(&buf)
(unsigned long) 0

?: circular_buffer_get_ready(&buf)
(unsigned long) 16

?: (buf.hbehind)
(bool) true

Final bits

Finally, do not forget to destroy the buffer:

?: circular_buffer_destroy(&buf);

Related tags: circular buffer, data structure, performance

Ouroboros - Circular Buffer - September 16, 2018 - Martin Di Paola