Skip to content
This repository has been archived by the owner on May 21, 2023. It is now read-only.

DNedic/lfbb_cpp

Repository files navigation

LFBB_CPP - Lock Free Bipartite Buffer

CMake

LFBB_CPP is now a part of lockfree, this repository has been locked and is not maintained separately anymore

LFBB_CPP is the C++ version of LFBB, a bipartite buffer implementation written in standard C++11, suitable for all platforms, from deeply embedded to HPC uses. It is lock-free for single consumer single producer scenarios making it incredibly performant and easy to use.

What is a bipartite buffer

A bipartite buffer is a variation of the classic ring buffer with the ability to always be able to provide the user with contigous memory regions for writing/reading if there is enough space/data. Here is a nice writeup about the essence of bipartite buffers.

Why use a bipartite buffer

A bipartite buffer should be used everywhere a ring buffer is used if you want:

  • To offload transfers to DMA increasing the transfer speed and freeing up CPU time
  • To avoid creating intermediate buffers for APIs that require contigous data
  • To process data inside the buffer without dequeing it
  • For scenarios where operations on data might fail or only some data might be used
  • To use stdlib memcpy which is faster than bytewise implementations used in most queues and ring buffers

Features

  • Written in standard C++11, compatible with all platforms supporting it
  • Lock free thread and multicore safe in single producer single consumer scenarios
  • No dynamic allocation
  • Optimized for high performance
  • MIT Licensed

Advantages over the C version

  • Type safety, as the buffer is type and size templated
  • Much simpler and less error-prone instantiation
  • Higher performance due to compile-time known size and header-only implementation
  • Encapsulation, the data buffer is a class member instead of being passed by a pointer

How to get

There are three main ways to get the library:

How to use

Shown here is an example of typical use:

  • Consumer thread/interrupt
auto read = lfbb_adc.ReadAcquire();

if (read.first != nullptr) {
    size_t data_used = DoStuffWithData(read);
    lfbb_adc.ReadRelease(data_used);
}
  • Producer thread/interrupt
if (!write_started) {
    auto *write_ptr = lfbb_adc.WriteAcquire(data.size());
    if (write_ptr != nullptr) {
        ADC_StartDma(&adc_dma_h, write_ptr, sizeof(data));
        write_started = true;
    }
} else {
    if (ADC_PollDmaComplete(&adc_dma_h) {
        lfbb_adc.WriteRelease(data.size());
        write_started = false;
    }
}

There is also a std::span based API for those using C++20 and up:

  • Consumer thread/interrupt
auto read = lfbb_adc.ReadAcquireSpan();

if (!read.empty())) {
    auto span_used = DoStuffWithData(read);
    lfbb_adc.ReadRelease(span_used);
}
  • Producer thread/interrupt
if (!write_started) {
    auto write_span = lfbb_adc.WriteAcquireSpan(data.size());
    if (!write_span.empty()) {
        ADC_StartDma(&adc_dma_h, write_span.data(), write_span.size_bytes());
        write_started = true;
    }
} else {
    if (ADC_PollDmaComplete(&adc_dma_h) {
        lfbb_adc.WriteRelease(data.size());
        write_started = false;
    }
}

Configuration

The library offers two configuration defines LFBB_MULTICORE_HOSTED and LFBB_CACHELINE_LENGTH that can be passed by the build system or defined before including the library if the configuration isn't suitable.

On embedded systems it is usually required to do manual cache synchronization, so LFBB_MULTICORE_HOSTED should be left as false to avoid wasting space on padding for cacheline alignment of indexes.

For hosted systems the False Sharing phenomenom can reduce performance to some extent which is why passing LFBB_MULTICORE_HOSTED as true is advisable. This aligns the indexes to the system cacheline size, 64 by default.

Some systems have a non-typical cacheline length (for instance the apple M1/M2 CPUs have a cacheline length of 128 bytes), and LFBB_CACHELINE_LENGTH should be set accordingly in those cases.

Dealing with caches on embedded systems

When using the library with DMA or asymmetric multicore on embedded systems with cache it is necessary to perform manual cache synchronization in one of the following ways:

  • Using platform specific data synchronization barriers (DSB on ARM)
  • By manually invalidating cache
  • By setting the MPU/MMU up to not cache the data buffer