Skip to content

Buffer Pool System

Thread-safe, RAII-based buffer pool for efficient memory management in real-time audio processing.

Overview

The buffer pool system provides pre-allocated reusable buffers to avoid dynamic memory allocation in performance-critical code paths (e.g., audio callbacks).

Key Features

  • Thread-safe: Lock-based synchronization for concurrent access
  • RAII: Automatic buffer release with ScopedBuffer
  • Optional acquisition: Non-throwing OptionalBuffer for graceful degradation
  • Cache-aligned: 64-byte alignment for optimal CPU cache performance
  • Peak tracking: Monitor maximum concurrent buffer usage
  • Auto-growth: Pool can grow up to 2x initial size when exhausted

Usage

Basic Pool Operations

#include "buffer_pool.hpp"

using namespace audiolab::core::buffer;

// Create pool with 10 pre-allocated buffers of 512 floats each
BufferPool<float, 512> pool(10);

// Manual acquire/release
float* buffer = pool.acquire();
// ... use buffer ...
pool.release(buffer);

// Check pool status
size_t total = pool.getTotalCount();
size_t available = pool.getAvailableCount();
size_t inUse = pool.getInUseCount();
#include "scoped_buffer.hpp"

void processAudio() {
    ScopedBuffer<float, 512> temp(globalPool);

    // Use buffer
    for (size_t i = 0; i < 512; ++i) {
        temp[i] = process(input[i]);
    }

    // Buffer automatically released when temp goes out of scope
}

Optional Acquisition (No Throw)

auto buffer = tryAcquireBuffer(pool);

if (buffer.isValid()) {
    // Use buffer
    for (size_t i = 0; i < buffer.size(); ++i) {
        buffer[i] = 0.0f;
    }
} else {
    // Pool exhausted - handle gracefully
    usePreallocatedFallback();
}

Move Semantics

ScopedBuffer<float, 256> createBuffer(BufferPool<float, 256>& pool) {
    return ScopedBuffer<float, 256>(pool);  // Move construct
}

auto buffer = createBuffer(pool);  // No double-release

Range-Based For Loop

ScopedBuffer<float, 512> buffer(pool);

// Initialize
for (auto& sample : buffer) {
    sample = 0.0f;
}

// Process
for (const auto& sample : buffer) {
    output += sample;
}

Real-World Example: Audio Effect

class Reverb {
    BufferPool<float, 1024> delayLinePool_{8};  // 8 delay line buffers

public:
    void process(float* input, float* output, size_t frames) {
        // Acquire temporary buffers
        ScopedBuffer<float, 1024> early(delayLinePool_);
        ScopedBuffer<float, 1024> late(delayLinePool_);

        // Early reflections
        for (size_t i = 0; i < frames; ++i) {
            early[i] = earlyReflections(input[i]);
        }

        // Late reflections
        for (size_t i = 0; i < frames; ++i) {
            late[i] = lateReverb(early[i]);
        }

        // Mix
        for (size_t i = 0; i < frames; ++i) {
            output[i] = input[i] * 0.7f + late[i] * 0.3f;
        }

        // Buffers automatically released here
    }
};

Thread Safety

The pool is thread-safe and can be shared across multiple threads:

BufferPool<float, 512> sharedPool(20);

void audioThread() {
    ScopedBuffer<float, 512> buffer(sharedPool);
    // ... process audio ...
}

void uiThread() {
    ScopedBuffer<float, 512> buffer(sharedPool);
    // ... analyze audio ...
}

Performance Considerations

DO:

  • ✅ Pre-allocate sufficient buffers for peak usage
  • ✅ Use ScopedBuffer for automatic cleanup
  • ✅ Check peak usage with getPeakUsage() to size pool correctly
  • ✅ Reuse pools across processing frames

DON'T:

  • ❌ Allocate pools in audio callback (pre-allocate in constructor)
  • ❌ Use pool for small buffers (< 64 bytes) - overhead not worth it
  • ❌ Grow pool excessively (indicates undersized initial allocation)

Diagnostics

// Monitor pool health
std::cout << "Total buffers: " << pool.getTotalCount() << "\n";
std::cout << "In use: " << pool.getInUseCount() << "\n";
std::cout << "Available: " << pool.getAvailableCount() << "\n";
std::cout << "Peak usage: " << pool.getPeakUsage() << "\n";

// Reset peak tracking
pool.resetPeakUsage();

// Verify buffer ownership (debugging)
assert(pool.ownsBuffer(myBuffer));

Error Handling

Exceptions Thrown

Exception Cause
std::invalid_argument Pool initialized with 0 buffers
std::invalid_argument Releasing buffer not from this pool
std::logic_error Double release detected
std::runtime_error ScopedBuffer when pool exhausted (use OptionalBuffer to avoid)

Exception Safety

All classes provide strong exception guarantee: - Buffers are always released, even during stack unwinding - No memory leaks on exceptions

Building and Testing

Compile Tests

# GCC/Clang
g++ -std=c++17 -pthread tests/test_pools.cpp -o test_pools
./test_pools

# MSVC
cl /std:c++17 tests/test_pools.cpp
test_pools.exe

CMake Integration

add_library(buffer_pools INTERFACE)
target_include_directories(buffer_pools INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(buffer_pools INTERFACE cxx_std_17)

# Tests
add_executable(test_buffer_pools tests/test_pools.cpp)
target_link_libraries(test_buffer_pools PRIVATE buffer_pools)

Implementation Details

Memory Layout

BufferPool<float, 512>:
  [PooledBuffer 0: alignas(64) float[512], bool inUse]
  [PooledBuffer 1: alignas(64) float[512], bool inUse]
  ...
  [Mutex for thread safety]
  [Peak usage counter]

Cache Alignment

Buffers are 64-byte aligned to prevent false sharing and optimize cache line usage:

struct PooledBuffer {
    alignas(64) T data[BufferSize];  // Aligned to cache line
    bool inUse{false};
};

Growth Strategy

  • Initial capacity: User-specified
  • Growth trigger: All buffers in use
  • Growth limit: 2× initial capacity
  • Growth amount: 1 buffer at a time

API Reference

BufferPool

Method Description
BufferPool(size_t count) Constructor with initial buffer count
T* acquire() Get buffer from pool (nullptr if exhausted)
void release(T* buffer) Return buffer to pool
size_t getAvailableCount() Number of free buffers
size_t getTotalCount() Total buffers in pool
size_t getInUseCount() Number of buffers currently in use
size_t getPeakUsage() Maximum concurrent buffers used
void resetPeakUsage() Reset peak counter to current usage
bool ownsBuffer(const T*) Check if buffer belongs to this pool
static constexpr size_t getBufferSize() Get buffer size

ScopedBuffer

Method Description
ScopedBuffer(BufferPool&) Acquire buffer (throws if exhausted)
~ScopedBuffer() Release buffer automatically
T* get() Get raw pointer
T& operator[](size_t) Array access
static constexpr size_t size() Get buffer size
begin() / end() Iterator support

OptionalBuffer

Method Description
OptionalBuffer(BufferPool&) Try to acquire (no throw)
bool isValid() Check if acquisition succeeded
T* get() Get raw pointer (nullptr if failed)
T& operator[](size_t) Array access

License

Part of AudioLab foundation library.