Skip to content

Commit 69c546a

Browse files
Charlie FraschCharlie Frasch
Charlie Frasch
authored and
Charlie Frasch
committed
More fifo types
1 parent 4a9c329 commit 69c546a

11 files changed

+345
-87
lines changed

CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@ add_fifo(fifo1)
4040
add_fifo(fifo2)
4141
add_fifo(fifo3)
4242
add_fifo(fifo4)
43+
add_fifo(fifo4a)
4344
add_fifo(fifo5)
4445
add_fifo(boost_lockfree)
4546
add_fifo(rigtorp)
4647
target_compile_options(rigtorp PRIVATE -Wno-interference-size)
4748
target_compile_options(rigtorp.tsan PRIVATE -Wno-interference-size)
49+
add_fifo(mutex)
50+
add_fifo(tryLock)
4851

4952

5053
include(GoogleTest)

Fifo1.hpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,6 @@ class Fifo1 : private Alloc
7777
private:
7878
size_type capacity_;
7979
T* ring_;
80-
size_type pushCursor_;
81-
size_type popCursor_;
80+
size_type pushCursor_{};
81+
size_type popCursor_{};
8282
};

Fifo4a.hpp

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#pragma once
2+
3+
#include <atomic>
4+
#include <cassert>
5+
#include <memory>
6+
#include <new>
7+
8+
9+
/// Threadsafe, efficient circular FIFO with cached cursors; bitwise AND vs remainder
10+
template<typename T, typename Alloc = std::allocator<T>>
11+
class Fifo4a : private Alloc
12+
{
13+
public:
14+
using value_type = T;
15+
using allocator_traits = std::allocator_traits<Alloc>;
16+
using size_type = typename allocator_traits::size_type;
17+
18+
explicit Fifo4a(size_type capacity, Alloc const& alloc = Alloc{})
19+
: Alloc{alloc}
20+
, mask_{capacity - 1}
21+
, ring_{allocator_traits::allocate(*this, capacity)}
22+
{}
23+
24+
~Fifo4a() {
25+
while(not empty()) {
26+
ring_[popCursor_ & mask_].~T();
27+
++popCursor_;
28+
}
29+
allocator_traits::deallocate(*this, ring_, capacity());
30+
}
31+
32+
/// Returns the number of elements in the fifo
33+
auto size() const noexcept {
34+
auto pushCursor = pushCursor_.load(std::memory_order_acquire);
35+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
36+
37+
assert(popCursor <= pushCursor);
38+
return pushCursor - popCursor;
39+
}
40+
41+
/// Returns whether the container has no elements
42+
auto empty() const noexcept { return size() == 0; }
43+
44+
/// Returns whether the container has capacity_() elements
45+
auto full() const noexcept { return size() == capacity(); }
46+
47+
/// Returns the number of elements that can be held in the fifo
48+
auto capacity() const noexcept { return mask_ + 1; }
49+
50+
51+
/// Push one object onto the fifo.
52+
/// @return `true` if the operation is successful; `false` if fifo is full.
53+
auto push(T const& value) {
54+
auto pushCursor = pushCursor_.load(std::memory_order_relaxed);
55+
if (full(pushCursor, popCursorCached_)) {
56+
popCursorCached_ = popCursor_.load(std::memory_order_acquire);
57+
}
58+
if (full(pushCursor, popCursorCached_)) {
59+
return false;
60+
}
61+
62+
new (&ring_[pushCursor & mask_]) T(value);
63+
pushCursor_.store(pushCursor + 1, std::memory_order_release);
64+
return true;
65+
}
66+
67+
/// Pop one object from the fifo.
68+
/// @return `true` if the pop operation is successful; `false` if fifo is empty.
69+
auto pop(T& value) {
70+
auto popCursor = popCursor_.load(std::memory_order_relaxed);
71+
if (empty(pushCursorCached_, popCursor)) {
72+
pushCursorCached_ = pushCursor_.load(std::memory_order_acquire);
73+
}
74+
if (empty(pushCursorCached_, popCursor)) {
75+
return false;
76+
}
77+
78+
value = ring_[popCursor & mask_];
79+
ring_[popCursor & mask_].~T();
80+
popCursor_.store(popCursor + 1, std::memory_order_release);
81+
return true;
82+
}
83+
84+
private:
85+
auto full(size_type pushCursor, size_type popCursor) const noexcept {
86+
return (pushCursor - popCursor) == capacity();
87+
}
88+
static auto empty(size_type pushCursor, size_type popCursor) noexcept {
89+
return pushCursor == popCursor;
90+
}
91+
92+
private:
93+
size_type mask_;
94+
T* ring_;
95+
96+
using CursorType = std::atomic<size_type>;
97+
static_assert(CursorType::is_always_lock_free);
98+
99+
// See Fifo3 for reason std::hardware_destructive_interference_size is not used directly
100+
static constexpr auto hardware_destructive_interference_size = size_type{64};
101+
102+
/// Loaded and stored by the push thread; loaded by the pop thread
103+
alignas(hardware_destructive_interference_size) CursorType pushCursor_;
104+
105+
/// Exclusive to the push thread
106+
alignas(hardware_destructive_interference_size) size_type popCursorCached_{};
107+
108+
/// Loaded and stored by the pop thread; loaded by the push thread
109+
alignas(hardware_destructive_interference_size) CursorType popCursor_;
110+
111+
/// Exclusive to the pop thread
112+
alignas(hardware_destructive_interference_size) size_type pushCursorCached_{};
113+
114+
// Padding to avoid false sharing with adjacent objects
115+
char padding_[hardware_destructive_interference_size - sizeof(size_type)];
116+
};

Fifo5.hpp

+26-67
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,6 @@
77
#include <new>
88
#include <type_traits>
99

10-
/// std::is_implicit_lifetime not implemented in g++-12
11-
/// Almost correct. See [P2674R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2674r0.pdf).
12-
template<typename T>
13-
struct is_implicit_lifetime : std::disjunction<
14-
std::is_scalar<T>,
15-
std::is_array<T>,
16-
std::is_aggregate<T>,
17-
std::conjunction<
18-
std::is_trivially_destructible<T>,
19-
std::disjunction<
20-
std::is_trivially_default_constructible<T>,
21-
std::is_trivially_copy_constructible<T>,
22-
std::is_trivially_move_constructible<T>>>> {};
23-
template<typename T>
24-
inline constexpr bool is_implicit_lifetime_v = is_implicit_lifetime<T>::value;
25-
26-
2710
/// A trait used to optimize the number of bytes copied. Specialize this
2811
/// on the type used to parameterize the Fifo5 to implement the
2912
/// optimization. The general template returns `sizeof(T)`.
@@ -35,9 +18,9 @@ struct ValueSizeTraits
3518
};
3619

3720

38-
/// Require implicit lifetime, add ValueSizeTraits, pusher and popper to Fifo4
21+
/// Require trivial, add ValueSizeTraits, pusher and popper to Fifo4
3922
template<typename T, typename Alloc = std::allocator<T>>
40-
requires is_implicit_lifetime_v<T>
23+
requires std::is_trivial_v<T>
4124
class Fifo5 : private Alloc
4225
{
4326
public:
@@ -47,19 +30,13 @@ class Fifo5 : private Alloc
4730

4831
explicit Fifo5(size_type capacity, Alloc const& alloc = Alloc{})
4932
: Alloc{alloc}
50-
, capacity_{capacity}
51-
52-
// allocate allocates n * sizeof(T) bytes of properly aligned but uninitialized
53-
// storage. Then it creates an array of type T[n] in the storage and starts its
54-
// lifetime, but ***does not start lifetime of any of its elements***.
55-
// Nowhere is "an array of type unsigned char or std::byte"
56-
// mentioned in the description of std::allocator<T>::allocate.
57-
, ring_{allocator_traits::allocate(*this, capacity)}
58-
// , ring_{std::start_lifetime_as_array(allocator_traits::allocate(*this, capacity), capacity)}
59-
{}
33+
, mask_{capacity - 1}
34+
, ring_{allocator_traits::allocate(*this, capacity)} {
35+
assert((capacity & mask_) == 0);
36+
}
6037

6138
~Fifo5() {
62-
allocator_traits::deallocate(*this, ring_, capacity_);
39+
allocator_traits::deallocate(*this, ring_, capacity());
6340
}
6441

6542

@@ -79,7 +56,7 @@ class Fifo5 : private Alloc
7956
auto full() const noexcept { return size() == capacity(); }
8057

8158
/// Returns the number of elements that can be held in the fifo
82-
auto capacity() const noexcept { return capacity_; }
59+
auto capacity() const noexcept { return mask_ + 1; }
8360

8461

8562
/// An RAII proxy object returned by push(). Allows the caller to
@@ -120,35 +97,23 @@ class Fifo5 : private Alloc
12097
/// Return whether or not the pusher_t is active.
12198
explicit operator bool() const noexcept { return fifo_; }
12299

123-
/// In-place construct `value_type` with @a args through std::forward.
124-
template< typename... Args >
125-
void emplace(Args &&... args) noexcept;
126-
127100
/// @name Direct access to the fifo's ring
128101
///@{
102+
value_type* get() noexcept { return fifo_->element(cursor_); }
103+
value_type const* get() const noexcept { return fifo_->element(cursor_); }
129104

130-
// QUESTION
131-
// These get() operations return a reference to one of the
132-
// elements in the T[n] array created by the allocate call
133-
// above. But the T's lifetime has not be started. Do I need to
134-
// call std::start_lifetime_as<T>(fifo->element(cursor_))?
135-
// If so, is it OK if get() happens to be called several times
136-
// on the same storage?
137-
auto& get() noexcept { return *fifo_->element(cursor_); }
138-
auto const& get() const noexcept { return *fifo_->element(cursor_); }
139-
140-
value_type& operator*() noexcept { return get(); }
141-
value_type const& operator*() const noexcept { return get(); }
142-
143-
value_type* operator->() noexcept { return &get(); }
144-
value_type const* operator->() const noexcept { return &get(); }
105+
value_type& operator*() noexcept { return *get(); }
106+
value_type const& operator*() const noexcept { return *get(); }
107+
108+
value_type* operator->() noexcept { return get(); }
109+
value_type const* operator->() const noexcept { return get(); }
145110
///@}
146111

147112
/// Copy-assign a `value_type` to the pusher. Prefer to use this
148113
/// form rather than assigning directly to a value_type&. It takes
149114
/// advantage of ValueSizeTraits.
150115
pusher_t& operator=(value_type const& value) noexcept {
151-
std::memcpy(&get(), std::addressof(value), ValueSizeTraits<value_type>::size(value));
116+
std::memcpy(get(), std::addressof(value), ValueSizeTraits<value_type>::size(value));
152117
return *this;
153118
}
154119

@@ -222,20 +187,14 @@ class Fifo5 : private Alloc
222187

223188
/// @name Direct access to the fifo's ring
224189
///@{
190+
value_type* get() noexcept { return fifo_->element(cursor_); }
191+
value_type const* get() const noexcept { return fifo_->element(cursor_); }
225192

226-
// QUESTION
227-
// If std::start_lifetime_as<T> must be called in the pusher_t
228-
// get() calls must it also be applied here? Or, are the
229-
// pusher_t calls to it and the memcpy sufficient to have an
230-
// actual object in the referenced t[] element?
231-
auto& get() noexcept { return *fifo_->element(cursor_); }
232-
auto const& get() const noexcept { return *fifo_->element(cursor_); }
233-
234-
value_type& operator*() noexcept { return get(); }
235-
value_type const& operator*() const noexcept { return get(); }
193+
value_type& operator*() noexcept { return *get(); }
194+
value_type const& operator*() const noexcept { return *get(); }
236195

237-
value_type* operator->() noexcept { return &get(); }
238-
value_type const* operator->() const noexcept { return &get(); }
196+
value_type* operator->() noexcept { return get(); }
197+
value_type const* operator->() const noexcept { return get(); }
239198
///@}
240199

241200
private:
@@ -269,17 +228,17 @@ class Fifo5 : private Alloc
269228
private:
270229
auto full(size_type pushCursor, size_type popCursor) const noexcept {
271230
assert(popCursor <= pushCursor);
272-
return (pushCursor - popCursor) == capacity_;
231+
return (pushCursor - popCursor) == capacity();
273232
}
274233
static auto empty(size_type pushCursor, size_type popCursor) noexcept {
275234
return pushCursor == popCursor;
276235
}
277236

278-
auto* element(size_type cursor) noexcept { return &ring_[cursor % capacity_]; }
279-
auto const* element(size_type cursor) const noexcept { return &ring_[cursor % capacity_]; }
237+
auto* element(size_type cursor) noexcept { return &ring_[cursor & mask_]; }
238+
auto const* element(size_type cursor) const noexcept { return &ring_[cursor & mask_]; }
280239

281240
private:
282-
size_type capacity_;
241+
size_type mask_;
283242
T* ring_;
284243

285244
using CursorType = std::atomic<size_type>;

Mutex.hpp

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#pragma once
2+
3+
#include <cstdlib>
4+
#include <memory>
5+
#include <mutex>
6+
7+
8+
/// Thread-safe, mutex-based FIFO
9+
template<typename T, typename Alloc = std::allocator<T>>
10+
class Mutex : private Alloc
11+
{
12+
public:
13+
using value_type = T;
14+
using allocator_traits = std::allocator_traits<Alloc>;
15+
using size_type = typename allocator_traits::size_type;
16+
17+
explicit Mutex(size_type capacity, Alloc const& alloc = Alloc{})
18+
: Alloc{alloc}
19+
, capacity_{capacity}
20+
, ring_{allocator_traits::allocate(*this, capacity)}
21+
{}
22+
23+
~Mutex() {
24+
while(not empty()) {
25+
ring_[popCursor_ % capacity_].~T();
26+
++popCursor_;
27+
}
28+
allocator_traits::deallocate(*this, ring_, capacity_);
29+
}
30+
31+
auto capacity() const noexcept { return capacity_; }
32+
auto size() const noexcept {
33+
std::lock_guard<std::mutex> lock(mutex_);
34+
return size(pushCursor_, popCursor_);
35+
}
36+
auto empty() const noexcept { return size() == 0; }
37+
auto full() const noexcept { return size() == capacity(); }
38+
39+
auto push(T const& value) {
40+
std::lock_guard<std::mutex> lock(mutex_);
41+
42+
if (full(pushCursor_, popCursor_)) {
43+
return false;
44+
}
45+
new (&ring_[pushCursor_ % capacity_]) T(value);
46+
++pushCursor_;
47+
return true;
48+
}
49+
50+
auto pop(T& value) {
51+
std::lock_guard<std::mutex> lock(mutex_);
52+
53+
if (empty(pushCursor_, popCursor_)) {
54+
return false;
55+
}
56+
value = ring_[popCursor_ % capacity_];
57+
ring_[popCursor_ % capacity_].~T();
58+
++popCursor_;
59+
return true;
60+
}
61+
62+
private:
63+
static auto size (size_type pushCursor, size_type popCursor) noexcept {
64+
return pushCursor - popCursor;
65+
}
66+
67+
auto full(size_type pushCursor, size_type popCursor) const noexcept {
68+
return (pushCursor - popCursor) == capacity_;
69+
}
70+
static auto empty(size_type pushCursor, size_type popCursor) noexcept {
71+
return pushCursor == popCursor;
72+
}
73+
74+
private:
75+
size_type capacity_;
76+
T* ring_;
77+
mutable std::mutex mutex_;
78+
size_type pushCursor_{};
79+
size_type popCursor_{};
80+
};

0 commit comments

Comments
 (0)