Skip to content

Circular queues

Letting head and tail wrap round to cell 0 turns a fixed array into a ring buffer, where the two ends chase each other for ever and no cell is ever lost.

Helps to know firstQueues

Helps to know first
This topic

First, the picture

Three items arrive each tick and two leave, in only eight cells. Watch head and tail go round and round.

A queue that wraps around

Eight cells in a ring; arrivals at the tail, departures at the head

bool put(struct ring *r, int v){    if (r->count == CAP)        return false;    r->items[r->tail] = v;    r->tail = (r->tail + 1) % CAP;    r->count++;    return true;} bool get(struct ring *r, int *v){    if (r->count == 0)        return false;    *v = r->items[r->head];    r->head = (r->head + 1) % CAP;    r->count--;    return true;} int main(void){    struct ring r = { .count = 0 };    int next = 1, refused = 0, served = 0;    for (int t = 0; t < TICKS; t++) {        for (int i = 0; i < IN; i++) {            if (!put(&r, next))                refused++;            next++;        }        for (int i = 0; i < OUT; i++) {            int v;            if (get(&r, &v))                served++;        }    }    printf("in %d per tick: %d arrived, %d served, %d refused, %d waiting\n",           IN, next - 1, served, refused, r.count);    printf("head %d, tail %d\n", r.head, r.tail);    return 0;}

main has started. r, a struct ring, is in main's frame: 8 ints, head, tail and count. 3 arrive each tick, up to 2 leave.

arrived
–
served
–
refused
–
waiting
–
3 in, 2 out

Output

0.00 / 22.40 s
Describe this picture

At the top, r.items, main’s array of 8 ints, is drawn as a ring: cell 0 at the top and the others clockwise, each cell with its index inside the ring and its value in the cell. A tag named tail sits outside the cell where the next item will go, and a tag named head outside the cell of the oldest item; both slide clockwise round the ring. Below, the same eight cells lie flat as a row numbered 0 to 7, tail above it and head below it. In both, the cells holding waiting items are drawn plainly and the others greyed; the cell a line just wrote is outlined. Under the picture are the lines of ring-buffer.c from put to the end, with the line about to run marked, one sentence on the current step, four readouts (arrived, served, refused, waiting), a slider for the arrivals per tick (1 to 4, with 2 leaving each tick) and the output. The clip plays once with 3 arrivals a tick, about 22 seconds. In tick 1 the first put runs line by line: 1 goes into cell 0 and tail moves to 1; 2 and 3 follow a beat each. The first get runs line by line: it reads 1 from cell 0 and head moves to 1, the cell greying with its 1 still in it; the second get follows in a beat. From tick 2 on, each tick takes two beats, one after the arrivals and one after the departures. In tick 3 tail passes cell 7 and lands on cell 1; in the flat row its tag jumps back to the left end. Each tick one more item is left waiting. In tick 6 the arrivals fill all eight cells, and the departures leave six; from tick 7 on, each tick’s arrivals fill the ring again, and the arrival that finds it full is refused: the cell at tail, which head also points at, turns to the warning colour. The clip rests after tick 10, with 6 waiting in cells 4, 5, 6, 7, 0 and 1, head at 4 and tail at 2. The output reads in 3 per tick: 30 arrived, 20 served, 4 refused, 6 waiting and head 4, tail 2. Moving the slider plays the run for that rate from the start. The transport plays and pauses, steps one line back or forward, and scrubs; the rate and a paused step are kept in the page address as ring.in and ring.step.

A ring with no end

In Queues, head and tail only ever moved right, so the queue ran out of room with six free cells behind its head. The fix is to let both indices go back to cell 0 when they pass the last cell. Cell 7 is then followed by cell 0, as if the array were bent round into a ring. The tail writes and moves on; the head reads and follows it round; neither ever reaches an end.

Nothing in memory moves or bends. The array is the same eight cells in a row, and the items stay where they were written: only the two indices go round. The picture draws the cells twice to show both views at once, the ring the indices see and the row that is really there.

The code and its cost

Wrapping the indices

#define CAP 8

struct ring {
    int items[CAP];
    int head, tail, count;
};

sizeof (struct ring) is 44 bytes: 32 for the array and 4 each for head, tail and count. In the clip r sits at 0x7fc0, so items[i] is at 0x7fc0 + 4i, head at 0x7fe0, tail at 0x7fe4 and count at 0x7fe8.

bool put(struct ring *r, int v)
{
    if (r->count == CAP)
        return false;
    r->items[r->tail] = v;
    r->tail = (r->tail + 1) % CAP;
    r->count++;
    return true;
}

bool get(struct ring *r, int *v)
{
    if (r->count == 0)
        return false;
    *v = r->items[r->head];
    r->head = (r->head + 1) % CAP;
    r->count--;
    return true;
}

(r->tail + 1) % CAP is the remainder after dividing by 8: for tail 0 to 6 it is one more, and for tail 7 it is 8 % 8 = 0. That one remainder is the whole trick. Because get can find the ring empty, it returns true or false and hands the item back through the pointer v.

When CAP is a power of two, the remainder can be worked out with a mask instead: (i + 1) & (CAP - 1) keeps only the low three bits of i + 1 when CAP is 8, which a processor does in a single instruction, faster than a division. Ring buffers in fast code are usually sized 64, 1024 or 4096 for that reason.

Full or empty?

When the ring is empty, head and tail point at the same cell. They also point at the same cell when it is full: tail has gone all the way round and caught up with head. head == tail cannot tell the two apart. The clip shows both. With 1 or 2 arrivals a tick the ring empties at the end of every tick, and ends with head and tail both at 2 or both at 4; with 3 a tick it fills at tick 6, when count reaches 8, and from tick 7 on it refuses arrivals, with head and tail together on a full ring.

There are two common ways out:

  • Keep a count, as this ring does: empty is count 0, full is count CAP.
  • Leave one cell unused: the ring is full when the cell after tail is head, so it holds at most CAP − 1 items, and head == tail means empty only.

When the ring is full

put has two choices. It can refuse the new item, as this one does, which suits a keyboard buffer: a key pressed while the buffer is full is lost, and the earlier keys are kept. Or it can overwrite the oldest item, by writing at tail and moving head on as well, which suits a log of the latest entries or the samples of an audio delay line, where only the newest matter.

What the arrival rate does

Each tick, IN items arrive and up to two leave. The slider under the picture runs the same program with IN from 1 to 4; each run lasts 10 ticks:

Arrivals per tickArrivedServedRefusedWaitinghead, tail
11010002, 2
22020004, 4
33020464, 2
440201464, 2

While arrivals keep up with departures, the ring never fills, however small it is. Once they outrun departures, it fills whatever its size: a larger ring only takes longer. With 3 a tick, one more item is left waiting each tick, 1, 2, 3, 4, 5, 6, and from tick 7 one arrival a tick is refused. With 3 or 4 a tick, 26 items were stored, so tail ended at 26 mod 8 = 2, and 20 were read, so head ended at 20 mod 8 = 4.

What it costs

put and get each take a fixed few steps, whatever the ring holds, and no item ever moves. Unlike the array queue of Queues, no cell is ever lost: every cell is used again once head has passed it. The price is a fixed capacity, chosen when the ring is made.

Running it yourself

gcc -std=c11 -Wall -Wextra -pedantic ring-buffer.c -o ring
./ring
gcc -std=c11 -Wall -Wextra -pedantic -DIN=4 ring-buffer.c -o ring4
./ring4

The first prints the row for 3 arrivals a tick, the second the row for 4.

Worked example

Where the items are. A ring of 8 cells with head 6 and tail 1 holds (1 − 6 + 8) % 8 = 3 items, in cells 6, 7 and 0. In general a ring that leaves no cell unused holds (tail − head + CAP) % CAP items, except when it is full, which is where the count comes in.

One cell unused. With CAP 8 and one cell left empty, the ring is full at 7 items, when the cell after tail is head.

An echo. An echo effect adds to each sound sample the sample from a quarter of a second ago, at half the strength: y[n] = x[n] + 0.5 x[n − 12,000], at 48,000 samples a second. It keeps the last 12,000 samples in a ring. Each new sample reads the oldest one, x[n − 12,000], from the cell at tail, then overwrites it with x[n] and moves tail on. A 10 ms delay at the same rate needs 480 cells. Real-time processing builds such delay lines.

Where it’s used

Keyboards and networks. A keyboard keeps the keys pressed but not yet read in a small ring buffer; a network card keeps arriving packets in a ring until the system takes them.

Audio. Delay lines, echoes and the memory of a filter’s past inputs live in ring buffers, where the index wraps exactly as in Circular vs linear convolution.

Logs. A log that keeps only the latest entries overwrites the oldest when it is full.

Between two threads. One thread puts data in at the tail while another takes it out at the head, and as long as each only moves its own index, they barely need to coordinate.

Reference card

Code or ideaMeaning
i = (i + 1) % CAPmove an index on, wrapping from CAP − 1 to 0
i = (i + 1) & (CAP - 1)the same, when CAP is a power of two
count == 0 / count == CAPempty / full (head == tail in both)
full when the cell after tail is headthe other way: one cell unused, CAP − 1 items
refuse / overwrite the oldestthe two choices when full
(tail - head + CAP) % CAPitems waiting, unless the ring is full
put, geta fixed few steps; no item moves, no cell is lost

Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look