Skip to content

Commit 76b65f2

Browse files
Charlie FraschCharlie Frasch
authored andcommitted
Add Fifo5b
1 parent c0b6ace commit 76b65f2

File tree

8 files changed

+281
-7
lines changed

8 files changed

+281
-7
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ add_fifo(fifo4a)
4444
add_fifo(fifo4b)
4545
add_fifo(fifo5)
4646
add_fifo(fifo5a)
47+
add_fifo(fifo5b)
4748
add_fifo(boost_lockfree)
4849
add_fifo(rigtorp)
4950
target_compile_options(rigtorp PRIVATE -Wno-interference-size)

Fifo5.hpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,7 @@ class Fifo5 : private Alloc
138138
/// Push one object onto the fifo.
139139
/// @return `true` if the operation is successful; `false` if fifo is full.
140140
auto push(T const& value) noexcept {
141-
auto pusher = push();
142-
if (pusher) {
141+
if (auto pusher = push(); pusher) {
143142
pusher = value;
144143
return true;
145144
}
@@ -216,8 +215,7 @@ class Fifo5 : private Alloc
216215
/// Pop one object from the fifo.
217216
/// @return `true` if the pop operation is successful; `false` if fifo is empty.
218217
auto pop(T& value) noexcept {
219-
auto popper = pop();
220-
if (popper) {
218+
if (auto popper = pop(); popper) {
221219
value = *popper;
222220
return true;
223221
}

Fifo5b.hpp

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
#pragma once
2+
3+
#include <atomic>
4+
#include <cassert>
5+
#include <cstring>
6+
#include <memory>
7+
#include <new>
8+
#include <type_traits>
9+
10+
// For ValueSizeTraits
11+
#include "Fifo5.hpp"
12+
13+
14+
/// Like Fifo5a except uses atomic_ref
15+
template<typename T, typename Alloc = std::allocator<T>>
16+
requires std::is_trivial_v<T>
17+
class Fifo5b : private Alloc
18+
{
19+
public:
20+
using value_type = T;
21+
using allocator_traits = std::allocator_traits<Alloc>;
22+
using size_type = typename allocator_traits::size_type;
23+
24+
explicit Fifo5b(size_type capacity, Alloc const& alloc = Alloc{})
25+
: Alloc{alloc}
26+
, mask_{capacity - 1}
27+
, ring_{allocator_traits::allocate(*this, capacity)} {
28+
assert((capacity & mask_) == 0);
29+
}
30+
31+
~Fifo5b() {
32+
allocator_traits::deallocate(*this, ring_, capacity());
33+
}
34+
35+
36+
/// Returns the number of elements in the fifo
37+
auto size() const noexcept {
38+
auto pushCursor = pushCursorRef_.load(std::memory_order_relaxed);
39+
auto popCursor = popCursorRef_.load(std::memory_order_relaxed);
40+
41+
assert(popCursor <= pushCursor);
42+
return pushCursor - popCursor;
43+
}
44+
45+
/// Returns whether the container has no elements
46+
auto empty() const noexcept { return size() == 0; }
47+
48+
/// Returns whether the container has capacity_() elements
49+
auto full() const noexcept { return size() == capacity(); }
50+
51+
/// Returns the number of elements that can be held in the fifo
52+
auto capacity() const noexcept { return mask_ + 1; }
53+
54+
55+
/// An RAII proxy object returned by push(). Allows the caller to
56+
/// manipulate value_type's members directly in the fifo's ring. The
57+
/// actual push happens when the pusher goes out of scope.
58+
class pusher_t
59+
{
60+
public:
61+
pusher_t() = default;
62+
explicit pusher_t(Fifo5b* fifo, size_type cursor) noexcept : fifo_{fifo}, cursor_{cursor} {}
63+
64+
pusher_t(pusher_t const&) = delete;
65+
pusher_t& operator=(pusher_t const&) = delete;
66+
67+
pusher_t(pusher_t&& other) noexcept
68+
: fifo_{std::move(other.fifo_)}
69+
, cursor_{std::move(other.cursor_)} {
70+
other.release();
71+
}
72+
pusher_t& operator=(pusher_t&& other) noexcept {
73+
fifo_ = std::move(other.fifo_);
74+
cursor_ = std::move(other.cursor_);
75+
other.release();
76+
return *this;
77+
}
78+
79+
~pusher_t() {
80+
if (fifo_) {
81+
fifo_->pushCursorRef_.store(cursor_ + 1, std::memory_order_release);
82+
}
83+
}
84+
85+
/// If called the actual push operation will not be called when the
86+
/// pusher_t goes out of scope. Operations on the pusher_t instance
87+
/// after release has been called are undefined.
88+
void release() noexcept { fifo_ = {}; }
89+
90+
/// Return whether or not the pusher_t is active.
91+
explicit operator bool() const noexcept { return fifo_; }
92+
93+
/// @name Direct access to the fifo's ring
94+
///@{
95+
value_type* get() noexcept { return fifo_->element(cursor_); }
96+
value_type const* get() const noexcept { return fifo_->element(cursor_); }
97+
98+
value_type& operator*() noexcept { return *get(); }
99+
value_type const& operator*() const noexcept { return *get(); }
100+
101+
value_type* operator->() noexcept { return get(); }
102+
value_type const* operator->() const noexcept { return get(); }
103+
///@}
104+
105+
/// Copy-assign a `value_type` to the pusher. Prefer to use this
106+
/// form rather than assigning directly to a value_type&. It takes
107+
/// advantage of ValueSizeTraits.
108+
pusher_t& operator=(value_type const& value) noexcept {
109+
std::memcpy(get(), std::addressof(value), ValueSizeTraits<value_type>::size(value));
110+
return *this;
111+
}
112+
113+
private:
114+
Fifo5b* fifo_{};
115+
size_type cursor_;
116+
};
117+
friend class pusher_t;
118+
119+
/// Optionally push one object onto a file via a pusher.
120+
/// @return a pointer to pusher_t.
121+
pusher_t push() noexcept {
122+
auto pushCursor = pushCursor_;
123+
if (full(pushCursor, popCursorCached_)) {
124+
// popCursorCached_ = popCursor_.load(std::memory_order_acquire);
125+
popCursorCached_ = popCursorRef_.load(std::memory_order_acquire);
126+
if (full(pushCursor, popCursorCached_)) {
127+
return pusher_t{};
128+
}
129+
}
130+
return pusher_t(this, pushCursor);
131+
}
132+
133+
/// Push one object onto the fifo.
134+
/// @return `true` if the operation is successful; `false` if fifo is full.
135+
auto push(T const& value) noexcept {
136+
if (auto pusher = push(); pusher) {
137+
pusher = value;
138+
return true;
139+
}
140+
return false;
141+
}
142+
143+
/// An RAII proxy object returned by pop(). Allows the caller to
144+
/// manipulate value_type members directly in the fifo's ring. The
145+
// /actual pop happens when the popper goes out of scope.
146+
class popper_t
147+
{
148+
public:
149+
popper_t() = default;
150+
explicit popper_t(Fifo5b* fifo, size_type cursor) noexcept : fifo_{fifo}, cursor_{cursor} {}
151+
152+
popper_t(popper_t const&) = delete;
153+
popper_t& operator=(popper_t const&) = delete;
154+
155+
popper_t(popper_t&& other) noexcept
156+
: fifo_{std::move(other.fifo_)}
157+
, cursor_{std::move(other.cursor_)} {
158+
other.release();
159+
}
160+
popper_t& operator=(popper_t&& other) noexcept {
161+
fifo_ = std::move(other.fifo_);
162+
cursor_ = std::move(other.cursor_);
163+
other.release();
164+
return *this;
165+
}
166+
167+
~popper_t() {
168+
if (fifo_) {
169+
fifo_->popCursorRef_.store(cursor_ + 1, std::memory_order_release);
170+
}
171+
}
172+
173+
/// If called the actual pop operation will not be called when the
174+
/// popper_t goes out of scope. Operations on the popper_t instance
175+
/// after release has been called are undefined.
176+
void release() noexcept { fifo_ = {}; }
177+
178+
/// Return whether or not the popper_t is active.
179+
explicit operator bool() const noexcept { return fifo_; }
180+
181+
/// @name Direct access to the fifo's ring
182+
///@{
183+
value_type* get() noexcept { return fifo_->element(cursor_); }
184+
value_type const* get() const noexcept { return fifo_->element(cursor_); }
185+
186+
value_type& operator*() noexcept { return *get(); }
187+
value_type const& operator*() const noexcept { return *get(); }
188+
189+
value_type* operator->() noexcept { return get(); }
190+
value_type const* operator->() const noexcept { return get(); }
191+
///@}
192+
193+
private:
194+
Fifo5b* fifo_{};
195+
size_type cursor_;
196+
};
197+
friend popper_t;
198+
199+
auto pop() noexcept {
200+
auto popCursor = popCursor_;
201+
if (empty(pushCursorCached_, popCursor)) {
202+
pushCursorCached_ = pushCursorRef_.load(std::memory_order_acquire);
203+
if (empty(pushCursorCached_, popCursor)) {
204+
return popper_t{};
205+
}
206+
}
207+
return popper_t(this, popCursor);
208+
};
209+
210+
/// Pop one object from the fifo.
211+
/// @return `true` if the pop operation is successful; `false` if fifo is empty.
212+
auto pop(T& value) noexcept {
213+
if (auto popper = pop(); popper) {
214+
value = *popper;
215+
return true;
216+
}
217+
return false;
218+
}
219+
220+
private:
221+
auto full(size_type pushCursor, size_type popCursor) const noexcept {
222+
assert(popCursor <= pushCursor);
223+
return (pushCursor - popCursor) == capacity();
224+
}
225+
static auto empty(size_type pushCursor, size_type popCursor) noexcept {
226+
return pushCursor == popCursor;
227+
}
228+
229+
auto* element(size_type cursor) noexcept { return &ring_[cursor & mask_]; }
230+
auto const* element(size_type cursor) const noexcept { return &ring_[cursor & mask_]; }
231+
232+
private:
233+
size_type mask_;
234+
T* ring_;
235+
236+
using CursorType = size_type;
237+
using CursorRefType = std::atomic_ref<CursorType>;
238+
239+
// https://stackoverflow.com/questions/39680206/understanding-stdhardware-destructive-interference-size-and-stdhardware-cons
240+
// See Fifo3.hpp for reason why std::hardware_destructive_interference_size is not used directly
241+
static constexpr auto hardware_destructive_interference_size = size_type{64};
242+
243+
/// Loaded and stored by the push thread; loaded by the pop thread
244+
alignas(hardware_destructive_interference_size) CursorType pushCursor_{};
245+
CursorRefType pushCursorRef_{pushCursor_};
246+
247+
/// Exclusive to the push thread
248+
alignas(hardware_destructive_interference_size) size_type popCursorCached_{};
249+
250+
/// Loaded and stored by the pop thread; loaded by the push thread
251+
alignas(hardware_destructive_interference_size) CursorType popCursor_{};
252+
CursorRefType popCursorRef_{popCursor_};
253+
254+
/// Exclusive to the pop thread
255+
alignas(hardware_destructive_interference_size) size_type pushCursorCached_{};
256+
257+
// Padding to avoid false sharing with adjacent objects
258+
char padding_[hardware_destructive_interference_size - sizeof(size_type)];
259+
};

bench.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "Fifo4.hpp"
22
#include "Fifo4a.hpp"
33
#include "Fifo5.hpp"
4+
#include "Fifo5a.hpp"
5+
#include "Fifo5b.hpp"
46
#include "rigtorp.hpp"
57

68
#include <benchmark/benchmark.h>
@@ -95,6 +97,8 @@ void BM_Fifo(benchmark::State& state) {
9597
BENCHMARK_TEMPLATE(BM_Fifo, Fifo4);
9698
BENCHMARK_TEMPLATE(BM_Fifo, Fifo4a);
9799
BENCHMARK_TEMPLATE(BM_Fifo, Fifo5);
100+
BENCHMARK_TEMPLATE(BM_Fifo, Fifo5a);
101+
BENCHMARK_TEMPLATE(BM_Fifo, Fifo5b);
98102
BENCHMARK_TEMPLATE(BM_Fifo, rigtorp::SPSCQueue);
99103

100104
BENCHMARK_MAIN();

bench_all.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "Fifo4b.hpp"
66
#include "Fifo5.hpp"
77
#include "Fifo5a.hpp"
8+
#include "Fifo5b.hpp"
89
#include "Mutex.hpp"
910
#include "rigtorp.hpp"
1011
#include <boost/lockfree/spsc_queue.hpp> // boost 1.74.0
@@ -35,6 +36,7 @@ void once(long iters, int cpu1, int cpu2) {
3536
Bench<Fifo4b<ValueT>>{}(iters, cpu1, cpu2) << "," << std::flush <<
3637
Bench<Fifo5<ValueT>>{}(iters, cpu1, cpu2) << "," << std::flush <<
3738
Bench<Fifo5a<ValueT>>{}(iters, cpu1, cpu2) << "," << std::flush <<
39+
Bench<Fifo5b<ValueT>>{}(iters, cpu1, cpu2) << "," << std::flush <<
3840
Bench<rigtorp::SPSCQueue<ValueT>>{}(iters, cpu1, cpu2) << "," << std::flush <<
3941
Bench<boost_spsc_queue<ValueT>>{}(iters, cpu1, cpu2) << std::flush <<
4042
"\n";
@@ -52,7 +54,7 @@ int main(int argc, char* argv[]) {
5254

5355
using value_type = std::int64_t;
5456

55-
std::cout << "Fifo3,Fifo4,Fifo4a,Fifo4b,Fifo5,Fifo5a,rigtorp,boost_spsc_queue" << std::endl;
57+
std::cout << "Fifo3,Fifo4,Fifo4a,Fifo4b,Fifo5,Fifo5a,Fifo5b,rigtorp,boost_spsc_queue" << std::endl;
5658
// std::cout << "Fifo2,Mutex\n";
5759
for (auto rep = 0; rep < reps; ++rep) {
5860
once<value_type>(iters, cpu1, cpu2);

fifo5b.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include "Fifo5b.hpp"
2+
#include "bench.hpp"
3+
4+
int main(int argc, char* argv[]) {
5+
bench<Fifo5b>("Fifo5b", argc, argv);
6+
}
7+

run_all.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#! /usr/bin/bash
22

3-
for bench in fifo2 fifo3 fifo4 fifo4a fifo4b fifo5 fifo5a rigtorp boost_lockfree mutex;
3+
for bench in fifo2 fifo3 fifo4 fifo4a fifo4b fifo5 fifo5a fifo5b rigtorp boost_lockfree mutex;
44
do
55
./build/release/$bench 1 2
66
done

unitTests.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include "Fifo3.hpp"
44
#include "Fifo4.hpp"
55
#include "Fifo5.hpp"
6+
#include "Fifo5a.hpp"
7+
#include "Fifo5b.hpp"
68

79
#include <gtest/gtest.h>
810

@@ -40,7 +42,8 @@ using FifoTypes = ::testing::Types<
4042
Fifo2<test_type>,
4143
Fifo3<test_type>,
4244
Fifo4<test_type>,
43-
Fifo5<test_type>
45+
Fifo5<test_type>,
46+
Fifo5b<test_type>
4447
>;
4548
TYPED_TEST_SUITE(FifoTest, FifoTypes);
4649

0 commit comments

Comments
 (0)