Generic ring memory
Nils Eilers, 23.06.2017
Grijjy, Inc. Consulting on GitHub: Ring Buffers
Grijjy, Inc. Consulting provides various libraries, components, and source code for classes used in their application development on GitHub. Part of this offering includes generic ring buffers, which are described in the following post:
What Is a Ring Buffer?
A ring buffer is a fixed-size buffer whose beginning and end are connected, forming a continuous loop. It resembles a queue with its FIFO (First-In-First-Out) behavior. However, unlike a queue, it does not grow when the buffer is full. This makes ring buffers very fast and memory-efficient.
Ring buffers are ideal for buffering data that is received unevenly but must be provided consistently. You can see this in action every time you watch or listen to streaming media: The media player buffers a few milliseconds to seconds of incoming data to be able to play it back at a steady rate. In media applications, such a buffer is also known as a jitter buffer, because it compensates for jitter.
We use a ring buffer in our Grijjy application to buffer audio for playback and voice recording.
An Example of a Ring Buffer Could Look Like This:
(You may now proceed to describe or implement the example if needed.)
This ring buffer has a fixed size of 16 elements (letters in this case). We have inserted 11 characters (A – K), so 5 are left.
We read from the ring buffer at the read pointer. For example, if we read 2 elements, we receive the characters A
and B
and move the read pointer forward by 2.
The ring buffer now has space for 7 more elements, which we can store at the write pointer. After 5 additional elements, we begin to overwrite the positions previously occupied by the characters A
and B
. For example, if we store 6 more characters, the buffer will look like this:
Here's the English translation:
A Generic Ring Buffer
Ring buffers can be useful for storing data of various types. Most commonly, you’ll find ring buffers that store generic data as byte buffers. However, in audio applications, for instance, it makes more sense to buffer audio samples, which are typically stored as 16-bit integers (Int16
or SmallInt
in Delphi) or as single-precision floating-point values (Single
in Delphi).
When audio is stored in an interleaved stereo format, each sample is represented as a record with two values (one for the left channel and one for the right channel).
So why not create a generic ring buffer that can be used with different types? We've created one for you called TgoRingBuffer<T>
. You can find it in the Grijjy.Collections
unit within the GrijjyFoundation repository.
Using the Ring Buffer
The API for the ring buffer is fairly simple:
type
TgoRingBuffer<T> = class
public
constructor Create(const ACapacity: Integer);
function Write(const AData: TArray<T>): Integer; overload;
function Write(const AData: array of T): Integer; overload;
function Write(const AData: array of T; const AIndex, ACount: Integer): Integer; overload;
function TryWrite(const AData: array of T): Boolean; overload;
function TryWrite(const AData: array of T; const AIndex, ACount: Integer): Boolean; overload;
function Read(var AData: array of T): Integer; overload;
function Read(var AData: array of T; const AIndex, ACount: Integer): Integer; overload;
function TryRead(var AData: array of T): Boolean; overload;
function TryRead(var AData: array of T; const AIndex, ACount: Integer): Boolean; overload;
property Capacity: Integer read FCapacity;
property Count: Integer read FCount;
property Available: Integer read GetAvailable;
end;
Explanation of the Ring Buffer API
The ring buffer is created by passing the capacity to the constructor.
This is the number of elements (of type
T
) that the ring buffer can hold.The buffer will never exceed this capacity.
Properties:
Capacity
: Returns the maximum number of elements that the ring buffer can hold.Count
: Returns the number of elements currently in the buffer (available for reading).Available
: Returns the number of available (free) items in the ring buffer (available for writing).
Note: All properties return the number of elements (of type
T
), not the number of bytes.
Read and Write Methods
The rest of the API consists of read and write methods with various overloads:
Array Flexibility:
Each method comes in two versions:One that accepts data as a dynamic array (
TArray<T>
).One that accepts data as an open array (
array of T
).
This allows for greater flexibility.
Pre-Allocated Array Reading:
The Read methods do not create an array. Instead, they expect a pre-allocated array (either static or dynamic).Regular vs. Try Methods:
The regular methods attempt to read or write a specified number of items.
If insufficient items (or space) are available, they will read or write as many as possible and return the actual count.
The
Try*
methods either succeed or fail completely. They attempt to read or write the specified number of items and return a boolean result indicating success or failure.
Reading and Writing Segments:
Each method has an overload for working with only a segment of an array.AIndex
: The starting index in the array.ACount
: The number of elements to read (or write), starting fromAIndex
.
Difference from Other Collections
Unlike other (generic) collections such as Lists, Stacks, or Queues, this ring buffer does not work with individual values. Instead, it reads or writes arrays of values.
This highlights that the ring buffer is typically used for streaming or buffering applications.
Example Usage
Let's say we want to replicate the previous example. We'll create a ring buffer with 16 elements of type Char
and write 11 letters (A
to K
) into it:
var
RingBuffer: TgoRingBuffer<Char>;
begin
RingBuffer := TgoRingBuffer<Char>.Create(16);
RingBuffer.Write(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']);
...
end;
Explanation of Ring Buffer Implementation
The ring buffer maintains a fixed capacity and operates in a circular manner.
When the buffer is full, new data overwrites the oldest data.
Data can be written to or read from the buffer using array operations.
The TgoRingBuffer<T>
implementation is particularly useful for:
Audio processing.
Network communication buffers.
Streaming data handling.
The source code is available on Grijjy’s GitHub repository.
Original post: “Expand your Collections collection – Part 2: a generic Ring Buffer” by Erik van Bilsen on Grijjy's website.
Translated by Nils Eilers.
We can also represent this as a "flat" array (which is actually the way the ring memory is implemented):
Explanation of the Code Snippet
The code snippet demonstrates how to read data from a TgoRingBuffer<Char>
into a static array.
Code
{ Reserve some space to read data from the ring buffer: }
var
ReadBuffer: array [0..9] of Char; { A static array with a capacity of 10 characters }
Count: Integer;
{ Read data }
Count := RingBuffer.Read(ReadBuffer, 0, 2); { Attempts to read 2 characters starting from index 0 }
Assert(Count = 2); { Ensures exactly 2 characters were read }
Explanation
Declare a Static Array:
ReadBuffer: array [0..9] of Char;
This defines a static array that can hold up to 10 characters.
Reading Data from the Ring Buffer:
Count := RingBuffer.Read(ReadBuffer, 0, 2);
The method attempts to read 2 characters from the ring buffer.
It starts writing data into the
ReadBuffer
at position 0.The
Read
method returns the actual number of characters read, stored in the variableCount
.
Checking the Result:
Assert(Count = 2);
This assertion ensures that exactly 2 characters were read from the ring buffer.
If the ring buffer contains fewer than 2 characters, the
Read
method will return a lower value, causing the assertion to fail.
Summary
The Read
method provides a flexible way to read data from the ring buffer. By passing a static array, you ensure that the data is read directly into memory without requiring dynamic memory allocation. This approach is particularly useful in performance-critical applications such as audio processing or network communication.
Would you like me to show you how to read more data or use the other API variations? 😊
✅ Explanation of the Code Snippet
You are now attempting to write 6 new elements into the ring buffer. Let's break down what's happening.
Code
Count := RingBuffer.Write(['L', 'M', 'N', 'O', 'P', 'Q']);
Assert(Count = 6);
What’s Happening Here?
Attempting to Write New Data:
Count := RingBuffer.Write(['L', 'M', 'N', 'O', 'P', 'Q']);
This line writes 6 new characters into the ring buffer.
The
Write
method returns the number of elements successfully written, which is stored in the variableCount
.
Assertion Check:
Assert(Count = 6);
This ensures that all 6 characters were successfully written to the buffer.
If the ring buffer is full or does not have enough available space, this assertion will fail.
Understanding the Ring Buffer State
Ring Buffer Capacity: 16 elements.
Initial Elements:
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'
.You previously read the first 2 elements (
'A', 'B'
), freeing 2 positions.Now, there are 5 more free positions available, making a total of:
5 (empty) + 2 (previously `'A'` and `'B'`) = 7 free positions.
You are attempting to write 6 new elements.
Expected Outcome
Writing
['L', 'M', 'N', 'O', 'P', 'Q']
should succeed, as there are 7 free positions available.The assertion
Assert(Count = 6);
should pass successfully.
📌 Summary
Your operation is valid because there is enough space in the buffer to accommodate all 6 elements. If you were to write more than 7 elements, the operation would partially succeed or fail, depending on whether you use a Write
or TryWrite
method.
Would you like me to show you how to read back these new elements from the buffer? 😊
✅ Explanation of the Code Snippet
You are now attempting to write 3 new elements to the ring buffer, but only 1 will fit. Let's break down why this happens.
Code
Count := RingBuffer.Write(['R', 'S', 'T']);
Assert(Count = 1);
What’s Happening Here?
Attempting to Write New Data:
Count := RingBuffer.Write(['R', 'S', 'T']);
You are trying to write 3 characters (
'R', 'S', 'T'
) to the buffer.The
Write
method will write as many characters as it can until the buffer reaches its maximum capacity.
Assertion Check:
Assert(Count = 1);
This confirms that only 1 element was successfully written to the buffer.
If more than 1 element had been written, the assertion would fail.
Understanding the Ring Buffer State
Total Capacity: 16 elements.
Current Elements in Buffer: 11 initial items minus the 2 read (
A
,B
), plus 6 new items:C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q
Total of 14 elements in the buffer.
Available Space:
16 - 14 = 2 slots available.
You tried to write 3 elements.
Since only 2 slots were available, only 1 item (
'R'
) fits.The ring buffer now contains 15 elements.
Expected Outcome
The method
Write(['R', 'S', 'T'])
will only write'R'
to the buffer.Count
will be1
.The assertion passes:
Assert(Count = 1);
📌 Summary
The buffer is almost full, with only 1 slot available.
Writing 3 elements is impossible, so only 1 is written.
This is confirmed by the successful assertion.
Would you like me to help you read all the items back from the buffer to confirm the current state? 😊
An dieser Stelle ist der Ringspeicher voll und kann nicht weiter beschrieben werden, bis Daten gelesen werden. Es können nun bis zu 16 Elemente (C bis R) gelesen werden.
Anwendungshinweise:
Wie Sie vielleicht in der Record Constraint der Klassendeklaration gesehen haben, kann der Ringspeicher nur mit Wertetypen (Wie Integer, Character, Floating-Point, Enums und Records) genutzt werden:
type TgoRingBuffer<T: record> = class ... end; |
Es ist nicht möglich, den Ringspeicher mit Referenztypen (Wie Strings und Objekte) zu verwenden.
Das ergibt Sinn, denn ein Ringspeicher wird zur Pufferung von Daten und nicht Objekten genutzt. Mit dieser Einschränkung können wir die Implementierung der Klasse optimieren und vereinfachen, da wir Referenztypen nicht berücksichtigen müssen.
Jetzt denken Sie vielleicht: „Na gut, dann verpacke ich einen Referenztyp einfach in einem Record“, wie hier:
type TWrappedString = record Value: String; end; var RingBuffer: TgoRingBuffer; |
Während das zwar kompiliert wird, wird eine Assertion in der Laufzeit ausgelöst, weil wir im Konstruktor prüfen, ob der Typ ein verwalteter Typ ist:
constructor TgoRingBuffer.Create(const ACapacity: Integer); begin Assert(not IsManagedType(T)); inherited Create; ... end; |
Die magische Kompilerfunktion IsManagedType gibt True zurück, wenn der gegebene Typ ein Speicherverwalteter Typ wie String oder Objekt ist. Sie gibt auch dann True zurück, wenn der Typ selbst nicht verwaltet ist (wie Records), aber einen verwalteten Typ enthält (Wie das String-Feld in dem obigen Beispiel).
Auf nicht-ARC-Plattformen werden Sie dennoch in der Lage sein, diesen „Hack“ zu benutzen, um ein Objekt zu verpacken. Das wird allerdings nicht auf ARC-Plattformen funktionieren, da diese auch Objekte verwalten.“
Nach dem Originalpost „Expand your Collections collection – Part 2: a generic ring buffer“, von Erik van Bilsen auf http://www.grijjy.com/, 12.01.2017. Übersetzung von Nils Eilers. Alle Bilder in der Übersetzung stammen ebenfalls aus dem Post „Expand your Collections collection – Part 2: a generic ring buffer“.