I have a a design philosophy that can be summed up in one sentence: "Make it work reliably and fast." This sentence can be broken down into the basic steps of a project:
1. Make it.
2. Make it work.
3. Make it work reliably.
4. Make it work reliably and fast.
The important thing is to approach a project in order. For instance a common mistake is to try to optimize a design (making it "fast") before you've laid down a foundation that works reliably. I tend to beat up on my developers if they are trying to find "clever" ways to optimize a design before they have the thing built, working, and bombproof. Usually they will miss a reliability issue whil trying to cut corners and can waste a ton of time.
I've worked on projects the took over a year to develop the first "proof of concept". It would be slow as heck, but bombproof. Then *after* everyone was satisfied with the functionality, we would start optiimizing. And you know what? We were able to optimize the project very quickly and make great performance because we had defined the entire project as a whole before looking at where time could be saved. Because we had the complete overall picture, we could see optimizations that we would have never seen if we had tried to optimize the individual parts separately before they were integrated. We also accounted for any reliably issues first and dealt with them before trying to speed things up. There's nothing more frustrating that trying to debug intermittent problems in cryptic, cleverly optimized code. Get those issues out of the way before you get clever and your life will be a lot easier.
Ok, why the lecture? Because I'm going the split the ring buffer discussion into four parts. Each will show one step in the process. The first three parts wil show what ring buffers are and how they work. The fourth part will show how to optimize them. By the time we get to the fourth part, you should understand ring buffers enough that the optimizations make sense. (You may want to punch me after reading the first part, but bear with me for a bit Besides, I'm having fun writing this stuff.
For Gauntlet, these optimizations made the difference between a playable game and an unplayable excercise in drawing graphics on a screen.
Hopefully at some point in the discussion everyone will get *something* out of it.
Ring Buffers:
What is a ring buffer?
A ring buffer conceptually is a First-In First-Out (FIFO) buffer that doesn't run out of space. It is implemented as a fixed sized array that uses seperate "Put" and "Get" pointers to store and retreive data from the array. Any type of data can be stored in the ring buffer, all the way from bytes to, pointers to objects, to actual objects. For the sake of this discussion we will only be storing bytes in the ring buffer, but the mechanics (and code) is the same for whatever data is stored in them.
A ring buffer is basically a place to temporily store data. It is commonly refered to as a "queue" or just generically as a FIFO. The terms "ring buffer", "queue", and "FIFO" are pretty much interchangable. Usually the same underlying code supports any of these programming abstractions. For instance, when someone says that they are queuing data, there is usually a ring buffer implemented to act as the queue.
Why would you need them?
Ring buffers allow two or more asyncronous processes to talk to each other. One process can put stuff into the ring buffer and sometime later on another process can take it out. The order that the data is put into the queue is preserved, so the FIFO keeps the data in sequence. (If you didn't stumble on me switching the terms around in this description, we're doing good, otherwise go back and read the last paragraph again.
Anytime you want to store data temporarily or buffer it over to to another process, a ring buffer is usually the approach you would take.
There are several advantages to using ring buffers:
1. Asyncronous passing of data between processes: Data can be passed between unrelated processes. There doesn't have to be a one-for-one relationship between putting the data into the ring buffer and taking it out. As long as the buffer doesn't overflow items can be put into the buffer and stored there until it's convienient to take them out. Sometimes a process will use a ring buffer to store data for itself asyncroniously - for example a command parser might gather enough keystrokes until it has enough an entire command sequence and then process it all at once.
2. Flow control: Ring buffers can be fitted with controls to throttle the process that is putting data into them so that the buffer doesn't overrun. This "handshaking" allows, for example, a high speed communication like to interface with a slower command parser.
3. Allows communication between seperate processes running in interrupt and background space: Passing data from an interrupt to a background task or vice versa is one of ths primary uses of ring buffers. The interrupt can react quickly to a communication or timer interrupt and then queue that work for later. This is how the ring buffers were used in Gauntlet. The ring buffer routines are re-entrant, so calling they can be called from an interrupt or from a background task without corrupting data.
4. Supports multiple sources and consumers of data: there is no limit to the number of data sources or syncs that a ring buffer can have. The ring buffer contents are stored in the order that they were received in *regardless* of the source, so it's also a way to sort a sequencing of events by time.
5. Can be monitored and used to optimize workload: Ring buffers can be fitted with counters to tell how much data is in them. This information can then be used to manage the workload across a number of data sources. I describe how Gauntlet does some of this in a previous post.
6. Keeps a history of data events which is useful for debugging: Because ring buffers have very low overhead, you can insert them into software components to track data streams. This allows you to dump them for dubugging purposes. For example, in the machine I'm currently working on, I get encoder updates as it spins. We use the encoder for positioning and product tracking. Although the software uses the encoder values immediately, it also stuffs them into a ring buffer. If we ever suspect that the machine is having an encoder problem, we can dump the contents of the ring buffer and see the last 1000 encoder updates. If there is a hardware problem, it becomes very obvious.
7. Very simple code: Ring buffer code is very simple and totally bombproof. One of the amazing thins is that as you optimize it for speed, the code actually gets *simpler*. That's becasue you can take advantage of microcomputer architecture to optimize some functionality that you would normally have to do in code. You'll see what I'm talking about as we get to the "and fast" part of the discussion.
So now we've laid the foundation and we can proceed to Step 1: Making our own ring buffer!














