diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bd4838..5446b03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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$<$:Debug>DLL") +endif () + add_library(spsc_ring_buffer INTERFACE) target_include_directories(spsc_ring_buffer INTERFACE diff --git a/docs/benchmark_report.md b/docs/benchmark_report.md index 7baa673..403b6ff 100644 --- a/docs/benchmark_report.md +++ b/docs/benchmark_report.md @@ -45,4 +45,25 @@ Performance drops from: This provides the motivation for exploring acquire/release ordering and cache-line optimization. ---- \ No newline at end of file +--- + +## 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. \ No newline at end of file diff --git a/include/spsc/ring_buffer.hpp b/include/spsc/ring_buffer.hpp index 77bc924..57b2040 100644 --- a/include/spsc/ring_buffer.hpp +++ b/include/spsc/ring_buffer.hpp @@ -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: