scc 2025.09
SystemC components library
SPSCQueue.h
1/*
2Copyright (c) 2020 Erik Rigtorp <erik@rigtorp.se>
3
4Permission is hereby granted, free of charge, to any person obtaining a copy
5of this software and associated documentation files (the "Software"), to deal
6in the Software without restriction, including without limitation the rights
7to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8copies of the Software, and to permit persons to whom the Software is
9furnished to do so, subject to the following conditions:
10
11The above copyright notice and this permission notice shall be included in all
12copies or substantial portions of the Software.
13
14THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20SOFTWARE.
21 */
22
23#pragma once
24
25#include <atomic>
26#include <cassert>
27#include <cstddef>
28#include <memory> // std::allocator
29#include <new> // std::hardware_destructive_interference_size
30#include <stdexcept>
31#include <type_traits> // std::enable_if, std::is_*_constructible
32
33#ifdef __has_cpp_attribute
34#if __has_cpp_attribute(nodiscard)
35#define RIGTORP_NODISCARD [[nodiscard]]
36#endif
37#endif
38#ifndef RIGTORP_NODISCARD
39#define RIGTORP_NODISCARD
40#endif
41
42namespace rigtorp {
43
44template <typename T, typename Allocator = std::allocator<T>> class SPSCQueue {
45
46#if defined(__cpp_if_constexpr) && defined(__cpp_lib_void_t)
47 template <typename Alloc2, typename = void> struct has_allocate_at_least : std::false_type {};
48
49 template <typename Alloc2>
50 struct has_allocate_at_least<Alloc2,
51 std::void_t<typename Alloc2::value_type, decltype(std::declval<Alloc2&>().allocate_at_least(size_t{}))>>
52 : std::true_type {};
53#endif
54
55public:
56 explicit SPSCQueue(const size_t capacity, const Allocator& allocator = Allocator())
57 : capacity_(capacity)
58 , allocator_(allocator) {
59 // The queue needs at least one element
60 if(capacity_ < 1) {
61 capacity_ = 1;
62 }
63 capacity_++; // Needs one slack element
64 // Prevent overflowing size_t
65 if(capacity_ > SIZE_MAX - 2 * kPadding) {
66 capacity_ = SIZE_MAX - 2 * kPadding;
67 }
68
69#if defined(__cpp_if_constexpr) && defined(__cpp_lib_void_t)
70 if constexpr(has_allocate_at_least<Allocator>::value) {
71 auto res = allocator_.allocate_at_least(capacity_ + 2 * kPadding);
72 slots_ = res.ptr;
73 capacity_ = res.count - 2 * kPadding;
74 } else {
75 slots_ = std::allocator_traits<Allocator>::allocate(allocator_, capacity_ + 2 * kPadding);
76 }
77#else
78 slots_ = std::allocator_traits<Allocator>::allocate(allocator_, capacity_ + 2 * kPadding);
79#endif
80
81 static_assert(alignof(SPSCQueue<T>) == kCacheLineSize, "");
82 static_assert(sizeof(SPSCQueue<T>) >= 3 * kCacheLineSize, "");
83 assert(reinterpret_cast<char*>(&readIdx_) - reinterpret_cast<char*>(&writeIdx_) >= static_cast<std::ptrdiff_t>(kCacheLineSize));
84 }
85
86 ~SPSCQueue() {
87 while(front()) {
88 pop();
89 }
90 std::allocator_traits<Allocator>::deallocate(allocator_, slots_, capacity_ + 2 * kPadding);
91 }
92
93 // non-copyable and non-movable
94 SPSCQueue(const SPSCQueue&) = delete;
95 SPSCQueue& operator=(const SPSCQueue&) = delete;
96
97 template <typename... Args> void emplace(Args&&... args) noexcept(std::is_nothrow_constructible<T, Args&&...>::value) {
98 static_assert(std::is_constructible<T, Args&&...>::value, "T must be constructible with Args&&...");
99 auto const writeIdx = writeIdx_.load(std::memory_order_relaxed);
100 auto nextWriteIdx = writeIdx + 1;
101 if(nextWriteIdx == capacity_) {
102 nextWriteIdx = 0;
103 }
104 while(nextWriteIdx == readIdxCache_) {
105 readIdxCache_ = readIdx_.load(std::memory_order_acquire);
106 }
107 new(&slots_[writeIdx + kPadding]) T(std::forward<Args>(args)...);
108 writeIdx_.store(nextWriteIdx, std::memory_order_release);
109 }
110
111 template <typename... Args>
112 RIGTORP_NODISCARD bool try_emplace(Args&&... args) noexcept(std::is_nothrow_constructible<T, Args&&...>::value) {
113 static_assert(std::is_constructible<T, Args&&...>::value, "T must be constructible with Args&&...");
114 auto const writeIdx = writeIdx_.load(std::memory_order_relaxed);
115 auto nextWriteIdx = writeIdx + 1;
116 if(nextWriteIdx == capacity_) {
117 nextWriteIdx = 0;
118 }
119 if(nextWriteIdx == readIdxCache_) {
120 readIdxCache_ = readIdx_.load(std::memory_order_acquire);
121 if(nextWriteIdx == readIdxCache_) {
122 return false;
123 }
124 }
125 new(&slots_[writeIdx + kPadding]) T(std::forward<Args>(args)...);
126 writeIdx_.store(nextWriteIdx, std::memory_order_release);
127 return true;
128 }
129
130 void push(const T& v) noexcept(std::is_nothrow_copy_constructible<T>::value) {
131 static_assert(std::is_copy_constructible<T>::value, "T must be copy constructible");
132 emplace(v);
133 }
134
135 template <typename P, typename = typename std::enable_if<std::is_constructible<T, P&&>::value>::type>
136 void push(P&& v) noexcept(std::is_nothrow_constructible<T, P&&>::value) {
137 emplace(std::forward<P>(v));
138 }
139
140 RIGTORP_NODISCARD bool try_push(const T& v) noexcept(std::is_nothrow_copy_constructible<T>::value) {
141 static_assert(std::is_copy_constructible<T>::value, "T must be copy constructible");
142 return try_emplace(v);
143 }
144
145 template <typename P, typename = typename std::enable_if<std::is_constructible<T, P&&>::value>::type>
146 RIGTORP_NODISCARD bool try_push(P&& v) noexcept(std::is_nothrow_constructible<T, P&&>::value) {
147 return try_emplace(std::forward<P>(v));
148 }
149
150 RIGTORP_NODISCARD T* front() noexcept {
151 auto const readIdx = readIdx_.load(std::memory_order_relaxed);
152 if(readIdx == writeIdxCache_) {
153 writeIdxCache_ = writeIdx_.load(std::memory_order_acquire);
154 if(writeIdxCache_ == readIdx) {
155 return nullptr;
156 }
157 }
158 return &slots_[readIdx + kPadding];
159 }
160
161 void pop() noexcept {
162 static_assert(std::is_nothrow_destructible<T>::value, "T must be nothrow destructible");
163 auto const readIdx = readIdx_.load(std::memory_order_relaxed);
164 assert(writeIdx_.load(std::memory_order_acquire) != readIdx && "Can only call pop() after front() has returned a non-nullptr");
165 slots_[readIdx + kPadding].~T();
166 auto nextReadIdx = readIdx + 1;
167 if(nextReadIdx == capacity_) {
168 nextReadIdx = 0;
169 }
170 readIdx_.store(nextReadIdx, std::memory_order_release);
171 }
172
173 RIGTORP_NODISCARD size_t size() const noexcept {
174 std::ptrdiff_t diff = writeIdx_.load(std::memory_order_acquire) - readIdx_.load(std::memory_order_acquire);
175 if(diff < 0) {
176 diff += capacity_;
177 }
178 return static_cast<size_t>(diff);
179 }
180
181 RIGTORP_NODISCARD bool empty() const noexcept {
182 return writeIdx_.load(std::memory_order_acquire) == readIdx_.load(std::memory_order_acquire);
183 }
184
185 RIGTORP_NODISCARD size_t capacity() const noexcept { return capacity_ - 1; }
186
187private:
188#ifdef __cpp_lib_hardware_interference_size
189 static constexpr size_t kCacheLineSize = std::hardware_destructive_interference_size;
190#else
191 static constexpr size_t kCacheLineSize = 64;
192#endif
193
194 // Padding to avoid false sharing between slots_ and adjacent allocations
195 static constexpr size_t kPadding = (kCacheLineSize - 1) / sizeof(T) + 1;
196
197private:
198 size_t capacity_;
199 T* slots_;
200#if defined(__has_cpp_attribute) && __has_cpp_attribute(no_unique_address)
201 Allocator allocator_ [[no_unique_address]];
202#else
203 Allocator allocator_;
204#endif
205
206 // Align to cache line size in order to avoid false sharing
207 // readIdxCache_ and writeIdxCache_ is used to reduce the amount of cache
208 // coherency traffic
209 alignas(kCacheLineSize) std::atomic<size_t> writeIdx_ = {0};
210 alignas(kCacheLineSize) size_t readIdxCache_ = 0;
211 alignas(kCacheLineSize) std::atomic<size_t> readIdx_ = {0};
212 alignas(kCacheLineSize) size_t writeIdxCache_ = 0;
213};
214} // namespace rigtorp