Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ project(SPSCRingbuffer VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# MSVC runtime
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif ()

add_library(spsc_ring_buffer INTERFACE)

target_include_directories(spsc_ring_buffer INTERFACE
Expand Down
23 changes: 22 additions & 1 deletion docs/benchmark_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,25 @@ Performance drops from:

This provides the motivation for exploring acquire/release ordering and cache-line optimization.

---
---

## v0.2 Optimization

### 📈 SPSC RingBuffer Acquire-Release (Issue #3)

* **Test Platform**: Apple M4 (10 Cores, macOS)
* **Compiler**: Apple Clang (`-O3` Release)
* **Memory Order**: `std::memory_order_relaxed`, `std::memory_order_release`, `std::memory_order_acquire`
* **Alignment**: Default Non-alignment (Maybe have false sharing)

| Capacity | Operation Latency(avg) | Throughput(ops/s) | 5-run std. dev. (CV) |
|:---------|:-----------------------|:------------------|:---------------------|
| **64** | ~15.46 ns | **64.65 M/s** | 6.98% |
| **1024** | ~14.94 ns | **66.95 M/s** | 9.37% |
| **4096** | ~16.52 ns | **60.53 M/s** | 11.86% |

### 👀 Observation

Switching from `seq_cst` to fine-grained Acquire-Release memory ordering yields only a minor performance gain (from **60.32M ops/s** to **66.95M ops/s**, ~10% improvement).

This key observation reveals that instruction reordering and full memory barriers are **not the primary bottleneck** at this stage. Instead, the system is severely memory-bound due to **False Sharing**: `write_idx` and `read_idx` reside on the same 64-byte cache line, causing constant cache-line invalidation (cache ping-pong) between CPU cores via the MESI protocol.
33 changes: 23 additions & 10 deletions include/spsc/ring_buffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,37 @@ class RingBuffer{
}

// producer
bool push(const T& item) {
if (full()) return false;
buffer_[write_idx.load() % capacity_] = item;
write_idx.fetch_add(1); // write_idx++; is okay
bool push(const T& item) {
const size_t current_w = write_idx.load(std::memory_order_relaxed);
const size_t current_r = read_idx.load(std::memory_order_acquire);
if (current_w - current_r == capacity_) return false;
buffer_[current_w % capacity_] = item;
write_idx.store(current_w + 1, std::memory_order_release);
return true;
}

// consumer
bool pop(T& item) {
if (empty()) return false;
item = buffer_[read_idx.load() % capacity_];
read_idx.fetch_add(1);
bool pop(T& item) {
const size_t current_r = read_idx.load(std::memory_order_relaxed);
const size_t current_w = write_idx.load(std::memory_order_acquire);
if (current_w == current_r) return false;
item = buffer_[current_r % capacity_];
read_idx.store(current_r + 1, std::memory_order_release);
return true;
}

bool empty() const { return write_idx.load() == read_idx.load(); }
bool empty() const {
return write_idx.load(std::memory_order_relaxed) == read_idx.load(std::memory_order_relaxed);
}

bool full() const { return size() == capacity_; }
size_t size() const { return write_idx.load() - read_idx.load(); }

size_t size() const {
// Approximate size in concurrent context.
// Not linearizable.
return write_idx.load(std::memory_order_relaxed) - read_idx.load(std::memory_order_relaxed);
}

size_t capacity() const { return capacity_; }

private:
Expand Down
Loading