que-es-buffer-circular-ring-buffer

¿Qué es un Buffer Circular?

  • 6 min

Un Buffer Circular o Ring Buffer es una estructura de datos que utiliza un único buffer de tamaño fijo como si sus extremos estuvieran conectados, formando un círculo.

Para dar la sensación de circular, usamos un “truquito”. Básicamente, usamos la memoria, pero cuando llegamos al final de esta, saltamos al principio (y viceversa).

programacion-circular-buffer

¿Para que este engendro espacial? Por rendimiento

¿Donde usarlo? Imagina que tienes que procesar datos que llegan sin parar (como audio, pulsaciones de teclado o datos de un sensor).

La memoria de tu ordenador es finita, pero el flujo de datos es potencialmente infinito. ¿Cómo lo gestionas eficientemente y sin que te explote el ordenador? Aquí es donde entra el Buffer Circular.

¿Por qué usar un Buffer Circular?

¿Por qué complicarnos la vida, y no usar simplmente una lista dinámica (List<T> o Vector) y voy borrando el primer elemento? Por rendimiento.

Complejidad O(1) siempre

Insertar y leer siempre cuesta lo mismo. No hay que redimensionar el array ni mover elementos de memoria. Borrar el primer elemento de un Array normal obliga a desplazar todos los demás una posición a la izquierda (). En el Buffer Circular, solo movemos un puntero.

Sin asignación dinámica

Al tener un tamaño fijo desde el principio, no estamos pidiendo y liberando memoria al Sistema Operativo constantemente. Esto es vital en sistemas embebidos o sistemas de alta velocidad.

Cache friendly

Al ser un array contiguo en memoria, se lleva muy bien con la caché del procesador.

¿Cómo funciona? La rica teoría de los punteros

Físicamente, en la memoria RAM, sigue siendo un array lineal normal y corriente. La “circularidad” es pura truquito matemática que aplicamos nosotros,

programacion-circular-buffer-1

Para gestionar este anillo, no movemos los datos de sitio (eso sería muy lento). Lo que movemos son dos índices (o punteros):

  1. Cabeza (Head / Write): Indica dónde vamos a escribir el siguiente dato que llegue.
  2. Cola (Tail / Read): Indica de dónde vamos a leer el siguiente dato para procesarlo.

De todo el array disponible los datos “vivos” son los que están entre la cola y la cabeza.

El ciclo de vida de los indices

Vamos a ver cómo funciona todo ese lio (que no es para tanto) de los índices, que es la gracia detrás del Circular Buffer.

Inicio: Cabeza y Cola están en la posición 0. El buffer está vacío.

Escribir: Añadimos datos y avanzamos la Cabeza. programacion-circular-buffer-2

Leer: Consumimos datos y avanzamos la Cola.

La Vuelta: Si la Cabeza llega al final del array, salta automáticamente a la posición 0 (siempre que haya hueco). programacion-circular-buffer-3

El problema del llenado: ¿Bloquear o sobrescribir?

La situación crítica en un Buffer Circular ocurre cuando la Cabeza alcanza a la Cola por detrás. Es decir, que hemos el buffer.

Aquí tenemos dos filosofías de diseño, dependiendo de para qué uses el buffer:

Modo “No perder nada” (Ej: Teclado, comunicaciones):

Si el buffer está lleno, rechazamos los nuevos datos o bloqueamos el proceso de escritura hasta que alguien lea y libere espacio. No podemos permitirnos perder una tecla que el usuario ha pulsado.

Modo “Siempre lo más nuevo” (Ej: Audio en tiempo real, sensores):

Si el buffer está lleno, la Cabeza sobrescribe lo que hay en la Cola y empuja la Cola hacia adelante.

Prefiero tener el dato del sensor de hace 1 milisegundo que uno de hace 10 minutos que no he podido procesar.

Implementación de un circular buffer en distintos lenguajes

Aunque el concepto es universal, la implementación varía según el lenguaje. A continuación, veremos ejemplos en varios lenguajes populares.

En Python, podemos implementar un circular buffer usando una lista y dos índices.

class CircularBuffer:
    def __init__(self, size):
        self.size = size
        self.buffer = [None] * size
        self.read_ptr = 0
        self.write_ptr = 0

    def insert(self, dato):
        self.buffer[self.write_ptr] = dato
        self.write_ptr = (self.write_ptr + 1) % self.size
        if self.write_ptr == self.read_ptr:
            self.read_ptr = (self.read_ptr + 1) % self.size

    def extract(self):
        if self.read_ptr == self.write_ptr:
            return None  # Buffer vacío
        dato = self.buffer[self.read_ptr]
        self.read_ptr = (self.read_ptr + 1) % self.size
        return dato

# Ejemplo de uso
buffer = CircularBuffer(5)
buffer.insert(1)
buffer.insert(2)
print(buffer.extract())  # Salida: 1
print(buffer.extract())  # Salida: 2
Copied!

En C++, podemos implementar un circular buffer usando un array y dos índices.

#include <iostream>
#include <vector>

class CircularBuffer {
public:
    CircularBuffer(int size) : size(size), buffer(size), read_ptr(0), write_ptr(0) {}

    void insert(int dato) {
        buffer[write_ptr] = dato;
        write_ptr = (write_ptr + 1) % size;
        if (write_ptr == read_ptr) {
            read_ptr = (read_ptr + 1) % size;
        }
    }

    int extract() {
        if (read_ptr == write_ptr) {
            return -1;  // Buffer vacío
        }
        int dato = buffer[read_ptr];
        read_ptr = (read_ptr + 1) % size;
        return dato;
    }

private:
    int size;
    std::vector<int> buffer;
    int read_ptr;
    int write_ptr;
};

int main() {
    CircularBuffer buffer(5);
    buffer.insert(1);
    buffer.insert(2);
    std::cout << buffer.extract() << std::endl;  // Salida: 1
    std::cout << buffer.extract() << std::endl;  // Salida: 2
    return 0;
}
Copied!

En Java, podemos implementar un circular buffer usando un array y dos índices.

public class CircularBuffer {
    private int size;
    private int[] buffer;
    private int readPtr;
    private int writePtr;

    public CircularBuffer(int size) {
        this.size = size;
        this.buffer = new int[size];
        this.readPtr = 0;
        this.writePtr = 0;
    }

    public void insert(int dato) {
        buffer[writePtr] = dato;
        writePtr = (writePtr + 1) % size;
        if (writePtr == readPtr) {
            readPtr = (readPtr + 1) % size;
        }
    }

    public int extract() {
        if (readPtr == writePtr) {
            return -1;  // Buffer vacío
        }
        int dato = buffer[readPtr];
        readPtr = (readPtr + 1) % size;
        return dato;
    }

    public static void main(String[] args) {
        CircularBuffer buffer = new CircularBuffer(5);
        buffer.insert(1);
        buffer.insert(2);
        System.out.println(buffer.extract());  // Salida: 1
        System.out.println(buffer.extract());  // Salida: 2
    }
}
Copied!

En JavaScript, podemos implementar un circular buffer usando un array y dos índices.

class CircularBuffer {
    constructor(size) {
        this.size = size;
        this.buffer = new Array(size);
        this.readPtr = 0;
        this.writePtr = 0;
    }

    insert(dato) {
        this.buffer[this.writePtr] = dato;
        this.writePtr = (this.writePtr + 1) % this.size;
        if (this.writePtr === this.readPtr) {
            this.readPtr = (this.readPtr + 1) % this.size;
        }
    }

    extract() {
        if (this.readPtr === this.writePtr) {
            return null;  // Buffer vacío
        }
        const dato = this.buffer[this.readPtr];
        this.readPtr = (this.readPtr + 1) % this.size;
        return dato;
    }
}

// Ejemplo de uso
const buffer = new CircularBuffer(5);
buffer.insert(1);
buffer.insert(2);
console.log(buffer.extract());  // Salida: 1
console.log(buffer.extract());  // Salida: 2
Copied!