Skip to content

Commit c76b336

Browse files
Charlie FraschCharlie Frasch
Charlie Frasch
authored and
Charlie Frasch
committed
Add Fifo3a, fix Fifo4b and Fifo5b
1 parent 76b65f2 commit c76b336

File tree

6 files changed

+223
-30
lines changed

6 files changed

+223
-30
lines changed

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ endfunction()
3939
add_fifo(fifo1)
4040
add_fifo(fifo2)
4141
add_fifo(fifo3)
42+
add_fifo(fifo3a)
4243
add_fifo(fifo4)
4344
add_fifo(fifo4a)
4445
add_fifo(fifo4b)

Fifo3a.hpp

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#pragma once
2+
3+
#include <atomic>
4+
#include <cassert>
5+
#include <memory>
6+
#include <new>
7+
8+
9+
/**
10+
* Threadsafe, efficient circular FIFO with constrained cursors
11+
*
12+
* This Fifo is useful when you need to constrain the cursor ranges. For
13+
* example say if the sizeof(CursorType) is 8 or 15. The cursors may
14+
* take on any value up to the fifo's capacity + 1. Furthermore, there
15+
* are no calculations where an intermediate cursor value is larger then
16+
* this number. And, finally, the cursors are never negative.
17+
*
18+
* The problem that must be resolved is how to distinguish an empty fifo
19+
* from a full one and still meet the above constraints. First, define
20+
* an empty fifo as when the pushCursor and popCursor are equal. We
21+
* cannot define a full fifo as pushCursor == popCursor + capacity.
22+
* Firstly, the intermediate value popCursor + capacity can overflow if
23+
* a signed cursor is used and if the cursors are constrained to
24+
* [0..capacity) there is no distiction between the full definition and
25+
* the empty definition.
26+
*
27+
* To resolve this we introduce the idea of a sentinal element by
28+
* allocating one more element than the capacity of the fifo and define
29+
* a full fifo as when the cursors are "one apart". That is,
30+
*
31+
* @code
32+
* | pushCursor < popCursor: pushCursor == popCursor - 1
33+
* | popCursor < pushCursor: popCursor == pushCursor - capacity
34+
* | else: false
35+
* @endcode
36+
*/
37+
template<typename T, typename Alloc = std::allocator<T>>
38+
class Fifo3a : private Alloc
39+
{
40+
public:
41+
using value_type = T;
42+
using allocator_traits = std::allocator_traits<Alloc>;
43+
using size_type = typename allocator_traits::size_type;
44+
45+
explicit Fifo3a(size_type capacity, Alloc const& alloc = Alloc{})
46+
: Alloc{alloc}
47+
, capacity_{capacity}
48+
, ring_{allocator_traits::allocate(*this, capacity + 1)}
49+
{}
50+
51+
~Fifo3a() {
52+
// TODO fix shouldn't matter for benchmark since it waits until
53+
// the fifo is empty and only need if destructors have side
54+
// effects.
55+
// while(not empty()) {
56+
// ring_[popCursor_ & mask_].~T();
57+
// ++popCursor_;
58+
// }
59+
allocator_traits::deallocate(*this, ring_, capacity_ + 1);
60+
}
61+
62+
/// Returns the number of elements in the fifo
63+
auto size() const noexcept {
64+
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
65+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
66+
if (popCursor <= pushCursor) {
67+
return pushCursor - popCursor;
68+
} else {
69+
return capacity_ - (popCursor - (pushCursor + 1));
70+
}
71+
}
72+
73+
/// Returns whether the container has no elements
74+
auto empty() const noexcept {
75+
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
76+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
77+
return empty(pushCursor, popCursor);
78+
}
79+
80+
/// Returns whether the container has capacity() elements
81+
auto full() const noexcept {
82+
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
83+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
84+
return full(pushCursor, popCursor);
85+
}
86+
87+
/// Returns the number of elements that can be held in the fifo
88+
auto capacity() const noexcept { return capacity_; }
89+
90+
91+
/// Push one object onto the fifo.
92+
/// @return `true` if the operation is successful; `false` if fifo is full.
93+
auto push(T const& value) {
94+
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
95+
auto popCursor = popCursor_.load(std::memory_order_acquire);
96+
if (full(pushCursor, popCursor)) {
97+
return false;
98+
}
99+
new (&ring_[pushCursor]) T(value);
100+
if (pushCursor == capacity_) {
101+
pushCursor_.store(0, std::memory_order_release);
102+
} else {
103+
pushCursor_.store(pushCursor + 1, std::memory_order_release);
104+
}
105+
return true;
106+
}
107+
108+
/// Pop one object from the fifo.
109+
/// @return `true` if the pop operation is successful; `false` if fifo is empty.
110+
auto pop(T& value) {
111+
auto pushCursor = pushCursor_.load(std::memory_order_acquire);
112+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
113+
if (empty(pushCursor, popCursor)) {
114+
return false;
115+
}
116+
value = ring_[popCursor];
117+
ring_[popCursor].~T();
118+
if (popCursor == capacity_) {
119+
popCursor_.store(0, std::memory_order_release);
120+
} else {
121+
popCursor_.store(popCursor + 1, std::memory_order_release);
122+
}
123+
return true;
124+
}
125+
126+
private:
127+
auto full(size_type pushCursor, size_type popCursor) const noexcept {
128+
if (pushCursor < popCursor) {
129+
return pushCursor == popCursor - 1;
130+
} else if (popCursor < pushCursor) {
131+
return popCursor == pushCursor - capacity_;
132+
} else {
133+
return false;
134+
}
135+
}
136+
static auto empty(size_type pushCursor, size_type popCursor) noexcept {
137+
return pushCursor == popCursor;
138+
}
139+
140+
private:
141+
size_type capacity_;
142+
T* ring_;
143+
144+
using CursorType = std::atomic<size_type>;
145+
static_assert(CursorType::is_always_lock_free);
146+
147+
// See Fifo3 for reason std::hardware_destructive_interference_size is not used directly
148+
static constexpr auto hardware_destructive_interference_size = size_type{64};
149+
150+
/// Loaded and stored by the push thread; loaded by the pop thread
151+
alignas(hardware_destructive_interference_size) CursorType pushCursor_;
152+
153+
/// Loaded and stored by the pop thread; loaded by the push thread
154+
alignas(hardware_destructive_interference_size) CursorType popCursor_;
155+
156+
// Padding to avoid false sharing with adjacent objects
157+
char padding_[hardware_destructive_interference_size - sizeof(CursorType)];
158+
};

Fifo4b.hpp

+45-28
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#include <new>
77

88

9-
/// Threadsafe, efficient circular FIFO with cached cursors; constrained cursors
9+
/// Threadsafe, efficient circular FIFO with constrained and cached cursors
1010
template<typename T, typename Alloc = std::allocator<T>>
1111
class Fifo4b : private Alloc
1212
{
@@ -17,83 +17,100 @@ class Fifo4b : private Alloc
1717

1818
explicit Fifo4b(size_type capacity, Alloc const& alloc = Alloc{})
1919
: Alloc{alloc}
20-
, capacity_{capacity + 1}
21-
, ring_{allocator_traits::allocate(*this, capacity)}
20+
, capacity_{capacity}
21+
, ring_{allocator_traits::allocate(*this, capacity + 1)}
2222
{}
2323

2424
~Fifo4b() {
2525
// TODO fix shouldn't matter for benchmark since it waits until
26-
// the fifo is empty
26+
// the fifo is empty and only need if destructors have side
27+
// effects.
2728
// while(not empty()) {
2829
// ring_[popCursor_ & mask_].~T();
2930
// ++popCursor_;
3031
// }
31-
allocator_traits::deallocate(*this, ring_, capacity());
32+
allocator_traits::deallocate(*this, ring_, capacity_ + 1);
3233
}
3334

3435
/// Returns the number of elements in the fifo
3536
auto size() const noexcept {
3637
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
3738
auto popCursor = popCursor_.load(std::memory_order_relaxed);
38-
39-
assert(popCursor <= pushCursor);
40-
return pushCursor - popCursor;
39+
if (popCursor <= pushCursor) {
40+
return pushCursor - popCursor;
41+
} else {
42+
return capacity_ - (popCursor - (pushCursor + 1));
43+
}
4144
}
4245

4346
/// Returns whether the container has no elements
44-
auto empty() const noexcept { return size() == 0; }
47+
auto empty() const noexcept {
48+
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
49+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
50+
return empty(pushCursor, popCursor);
51+
}
4552

4653
/// Returns whether the container has capacity() elements
47-
auto full() const noexcept { return size() == capacity(); }
54+
auto full() const noexcept {
55+
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
56+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
57+
return full(pushCursor, popCursor);
58+
}
4859

4960
/// Returns the number of elements that can be held in the fifo
50-
auto capacity() const noexcept { return capacity_ - 1; }
61+
auto capacity() const noexcept { return capacity_; }
5162

5263

5364
/// Push one object onto the fifo.
5465
/// @return `true` if the operation is successful; `false` if fifo is full.
5566
auto push(T const& value) {
5667
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
57-
auto nextPushCursor = pushCursor + 1;
58-
if (nextPushCursor == capacity_) {
59-
nextPushCursor = 0;
60-
}
61-
if (nextPushCursor == popCursorCached_) {
68+
if (full(pushCursor, popCursorCached_)) {
6269
popCursorCached_ = popCursor_.load(std::memory_order_acquire);
63-
if (nextPushCursor == popCursorCached_) {
70+
if (full(pushCursor, popCursorCached_)) {
6471
return false;
6572
}
6673
}
6774

6875
new (&ring_[pushCursor]) T(value);
69-
pushCursor_.store(nextPushCursor, std::memory_order_release);
76+
if (pushCursor == capacity_) {
77+
pushCursor_.store(0, std::memory_order_release);
78+
} else {
79+
pushCursor_.store(pushCursor + 1, std::memory_order_release);
80+
}
7081
return true;
7182
}
7283

7384
/// Pop one object from the fifo.
7485
/// @return `true` if the pop operation is successful; `false` if fifo is empty.
7586
auto pop(T& value) {
7687
auto popCursor = popCursor_.load(std::memory_order_relaxed);
77-
if (popCursor == pushCursorCached_) {
78-
pushCursorCached_ = pushCursor_.load(std::memory_order_acquire);
79-
if (pushCursorCached_ == popCursor) {
80-
return false;
81-
}
88+
if (empty(pushCursorCached_, popCursor)) {
89+
pushCursorCached_ = pushCursor_.load(std::memory_order_acquire);
90+
if (empty(pushCursorCached_, popCursor)) {
91+
return false;
92+
}
8293
}
8394

8495
value = ring_[popCursor];
8596
ring_[popCursor].~T();
86-
auto nextPopCursor = popCursor + 1;
87-
if (nextPopCursor == capacity_) {
88-
nextPopCursor = 0;
97+
if (popCursor == capacity_) {
98+
popCursor_.store(0, std::memory_order_release);
99+
} else {
100+
popCursor_.store(popCursor + 1, std::memory_order_release);
89101
}
90-
popCursor_.store(nextPopCursor, std::memory_order_release);
91102
return true;
92103
}
93104

94105
private:
95106
auto full(size_type pushCursor, size_type popCursor) const noexcept {
96-
return (pushCursor - popCursor) == capacity();
107+
if (pushCursor < popCursor) {
108+
return pushCursor == popCursor - 1;
109+
} else if (popCursor < pushCursor) {
110+
return popCursor == pushCursor - capacity_;
111+
} else {
112+
return false;
113+
}
97114
}
98115
static auto empty(size_type pushCursor, size_type popCursor) noexcept {
99116
return pushCursor == popCursor;

Fifo5b.hpp

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ class Fifo5b : private Alloc
2828
assert((capacity & mask_) == 0);
2929
}
3030

31+
// For consistency with the other fifos
32+
Fifo5b(Fifo5b const&) = delete;
33+
Fifo5b(Fifo5b&&) = delete;
34+
3135
~Fifo5b() {
3236
allocator_traits::deallocate(*this, ring_, capacity());
3337
}

fifo3a.cpp

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

unitTests.cpp

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#include "Fifo1.hpp"
22
#include "Fifo2.hpp"
33
#include "Fifo3.hpp"
4+
#include "Fifo3a.hpp"
45
#include "Fifo4.hpp"
6+
#include "Fifo4b.hpp"
57
#include "Fifo5.hpp"
68
#include "Fifo5a.hpp"
79
#include "Fifo5b.hpp"
@@ -41,7 +43,9 @@ using FifoTypes = ::testing::Types<
4143
Fifo1<test_type>,
4244
Fifo2<test_type>,
4345
Fifo3<test_type>,
46+
Fifo3a<test_type>,
4447
Fifo4<test_type>,
48+
Fifo4b<test_type>,
4549
Fifo5<test_type>,
4650
Fifo5b<test_type>
4751
>;
@@ -118,17 +122,20 @@ TYPED_TEST(FifoTest, popFullFifo) {
118122
EXPECT_FALSE(this->fifo.pop(value));
119123

120124
for (auto i = 0u; i < this->fifo.capacity(); ++i) {
121-
this->fifo.push(42 + i);
125+
ASSERT_EQ(i, this->fifo.size());
126+
ASSERT_TRUE(this->fifo.push(42 + i));
122127
}
128+
EXPECT_EQ(this->fifo.capacity(), this->fifo.size());
123129
EXPECT_TRUE(this->fifo.full());
124130

125131
for (auto i = 0u; i < this->fifo.capacity()*4; ++i) {
126132
EXPECT_TRUE(this->fifo.pop(value));
127133
EXPECT_EQ(42 + i, value);
128-
EXPECT_FALSE(this->fifo.full());
134+
EXPECT_FALSE(this->fifo.full());
129135

130136
EXPECT_TRUE(this->fifo.push(42 + 4 + i));
131137
EXPECT_TRUE(this->fifo.full());
138+
EXPECT_EQ(this->fifo.capacity(), this->fifo.size());
132139
}
133140
}
134141

0 commit comments

Comments
 (0)