Skip to content

Commit b041b3f

Browse files
committed
✨ Add atomic
Problem: - Using `std::atomic` does not necessarily produce good codegen for microcontroller platforms. Solution: - Add `stdx::atomic` which uses the customizable atomic interface from the baremetal concurrency library.
1 parent 99231b9 commit b041b3f

File tree

13 files changed

+391
-6
lines changed

13 files changed

+391
-6
lines changed

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ endif()
1616

1717
add_versioned_package("gh:boostorg/mp11#boost-1.83.0")
1818
fmt_recipe(10.2.1)
19-
add_versioned_package("gh:intel/cpp-baremetal-concurrency#27de8e1")
19+
add_versioned_package("gh:intel/cpp-baremetal-concurrency#06e5901")
2020

2121
if(NOT DEFINED CMAKE_CXX_STANDARD)
2222
set(CMAKE_CXX_STANDARD 20)
@@ -39,6 +39,7 @@ target_sources(
3939
include
4040
FILES
4141
include/stdx/algorithm.hpp
42+
include/stdx/atomic.hpp
4243
include/stdx/atomic_bitset.hpp
4344
include/stdx/bit.hpp
4445
include/stdx/bitset.hpp

docs/atomic.adoc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
== `atomic.hpp`
3+
4+
https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic.hpp[`atomic.hpp`]
5+
provides an implementation of
6+
https://en.cppreference.com/w/cpp/atomic/atomic[`std::atomic`] with a few
7+
differences.
8+
9+
`stdx::atomic` does not implement:
10+
11+
* `is_lock_free` or `is_always_lock_free`
12+
* `compare_exchange_{weak,strong}`
13+
* `wait`
14+
* `notify_{one,all}`
15+
* `fetch_{max,min}`
16+
17+
However, `stdx::atomic` allows customization of the atomic implementation for
18+
best codegen. `stdx::atomic` is implemented using the atomic API exposed by
19+
Intel's https://github.com/intel/cpp-baremetal-concurrency[baremetal concurrency
20+
library].
21+
22+
For example, it is possible that a particular platform requires atomic accesses
23+
to be 32-bit aligned. To achieve that for `stdx::atomic<bool>`, we could provide a
24+
configuration header specializing `::atomic::alignment_of`:
25+
26+
[source,cpp]
27+
----
28+
// this header: atomic_cfg.hpp
29+
#include <cstdint>
30+
31+
template <>
32+
constexpr inline auto ::atomic::alignment_of<bool> = alignof(std::uint32_t);
33+
----
34+
35+
To apply this configuration, when compiling, pass `-DATOMIC_CFG="<path>/atomic_cfg.hpp"`.
36+
The result would be that `stdx::atomic<bool>` has 32-bit alignment:
37+
38+
[source,cpp]
39+
----
40+
static_assert(alignof(stdx::atomic<bool>) == alignof(std::uint32_t));
41+
----
42+
43+
Using the https://github.com/intel/cpp-baremetal-concurrency[baremetal
44+
concurrency library] it is possible to override the handling of atomic access
45+
(`load`, `store`, `exchange`, `fetch_<op>`) to ensure the best codegen on a
46+
particular platform. As well as alignment concerns, for instance it may be the
47+
case on a single-core microcontroller that it is cheaper to disable and
48+
re-enable interrupts around a read/write than incurring a lock-free atomic
49+
access.

docs/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
:toc: left
88

99
include::intro.adoc[]
10+
include::atomic.adoc[]
1011
include::atomic_bitset.adoc[]
1112
include::algorithm.adoc[]
1213
include::bit.adoc[]

docs/intro.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ into headers whose names match the standard.
3535
The following headers are available:
3636

3737
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/algorithm.hpp[`algorithm.hpp`]
38+
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic.hpp[`atomic.hpp`]
3839
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic_bitset.hpp[`atomic_bitset.hpp`]
3940
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bit.hpp[`bit.hpp`]
4041
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bitset.hpp[`bitset.hpp`]

include/stdx/atomic.hpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#pragma once
2+
3+
#include <conc/atomic.hpp>
4+
5+
#include <atomic>
6+
#include <type_traits>
7+
8+
#if __cplusplus >= 202002L
9+
#define CPP20(...) __VA_ARGS__
10+
#else
11+
#define CPP20(...)
12+
#endif
13+
14+
namespace stdx {
15+
inline namespace v1 {
16+
template <typename T> class atomic {
17+
static_assert(std::is_trivially_copyable_v<T> and
18+
std::is_copy_constructible_v<T> and
19+
std::is_move_constructible_v<T> and
20+
std::is_copy_assignable_v<T> and
21+
std::is_move_assignable_v<T>,
22+
"Atomic values must be trivially copyable, copy "
23+
"constructible and copy assignable");
24+
25+
using elem_t = ::atomic::atomic_type_t<T>;
26+
constexpr static auto alignment = ::atomic::alignment_of<T>;
27+
28+
static_assert(std::is_convertible_v<elem_t, T>,
29+
"::atomic::atomic_type is specialized, but the type it "
30+
"produces is not convertible to T");
31+
static_assert(std::is_convertible_v<T, elem_t>,
32+
"::atomic::atomic_type is specialized, but the type it "
33+
"produces is not convertible from T");
34+
35+
alignas(alignment) elem_t value;
36+
37+
public:
38+
using value_type = T;
39+
40+
constexpr atomic()
41+
#if __cplusplus >= 202002L
42+
requires std::is_default_constructible_v<elem_t>
43+
#endif
44+
: value{} {
45+
}
46+
constexpr atomic(T t) : value{static_cast<elem_t>(t)} {}
47+
atomic(atomic const &) = delete;
48+
auto operator=(atomic const &) -> atomic & = delete;
49+
50+
[[nodiscard]] auto
51+
load(std::memory_order mo = std::memory_order_seq_cst) const -> T {
52+
return static_cast<T>(::atomic::load(value, mo));
53+
}
54+
55+
void store(T t, std::memory_order mo = std::memory_order_seq_cst) {
56+
::atomic::store(value, static_cast<elem_t>(t), mo);
57+
}
58+
59+
operator T() const { return load(); }
60+
auto operator=(T t) -> T {
61+
store(t);
62+
return t;
63+
}
64+
65+
[[nodiscard]] auto
66+
exchange(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
67+
return ::atomic::exchange(value, static_cast<elem_t>(t), mo);
68+
}
69+
70+
auto fetch_add(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
71+
CPP20(static_assert(
72+
requires { t + t; }, "operation is not supported on T"));
73+
return ::atomic::fetch_add(value, static_cast<elem_t>(t), mo);
74+
}
75+
auto fetch_sub(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
76+
CPP20(static_assert(
77+
requires { t - t; }, "operation is not supported on T"));
78+
return ::atomic::fetch_sub(value, static_cast<elem_t>(t), mo);
79+
}
80+
81+
auto operator+=(T t) -> T { return fetch_add(t) + t; }
82+
auto operator-=(T t) -> T { return fetch_sub(t) - t; }
83+
84+
auto operator++() -> T {
85+
CPP20(static_assert(
86+
requires(T t) { ++t; }, "Operation is not supported on T"));
87+
return fetch_add(1) + 1;
88+
}
89+
[[nodiscard]] auto operator++(int) -> T {
90+
CPP20(static_assert(
91+
requires(T t) { t++; }, "Operation is not supported on T"));
92+
return fetch_add(1);
93+
}
94+
auto operator--() -> T {
95+
CPP20(static_assert(
96+
requires(T t) { --t; }, "Operation is not supported on T"));
97+
return fetch_sub(1) - 1;
98+
}
99+
[[nodiscard]] auto operator--(int) -> T {
100+
CPP20(static_assert(
101+
requires(T t) { t--; }, "Operation is not supported on T"));
102+
return fetch_sub(1);
103+
}
104+
105+
auto fetch_and(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
106+
CPP20(static_assert(
107+
requires { t & t; }, "Operation is not supported on T"));
108+
return ::atomic::fetch_and(value, static_cast<elem_t>(t), mo);
109+
}
110+
auto fetch_or(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
111+
CPP20(static_assert(
112+
requires { t | t; }, "Operation is not supported on T"));
113+
return ::atomic::fetch_or(value, static_cast<elem_t>(t), mo);
114+
}
115+
auto fetch_xor(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
116+
CPP20(static_assert(
117+
requires { t ^ t; }, "Operation is not supported on T"));
118+
return ::atomic::fetch_xor(value, static_cast<elem_t>(t), mo);
119+
}
120+
121+
auto operator&=(T t) -> T { return fetch_and(t) & t; }
122+
auto operator|=(T t) -> T { return fetch_or(t) | t; }
123+
auto operator^=(T t) -> T { return fetch_xor(t) ^ t; }
124+
};
125+
} // namespace v1
126+
} // namespace stdx

test/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ add_tests(
2121
FILES
2222
algorithm
2323
always_false
24+
atomic
25+
atomic_override
2426
atomic_bitset
2527
atomic_bitset_override
2628
bind
@@ -65,6 +67,10 @@ target_compile_definitions(
6567
atomic_bitset_override_test
6668
PRIVATE -DATOMIC_CFG="${CMAKE_CURRENT_LIST_DIR}/detail/atomic_cfg.hpp")
6769

70+
target_compile_definitions(
71+
atomic_override_test
72+
PRIVATE -DATOMIC_CFG="${CMAKE_CURRENT_LIST_DIR}/detail/atomic_cfg.hpp")
73+
6874
if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20)
6975
add_tests(FILES ct_format ct_string indexed_tuple tuple tuple_algorithms)
7076
endif()

test/atomic.cpp

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#include <stdx/atomic.hpp>
2+
3+
#include <catch2/catch_template_test_macros.hpp>
4+
#include <catch2/catch_test_macros.hpp>
5+
6+
#include <cstdint>
7+
#include <type_traits>
8+
9+
TEMPLATE_TEST_CASE("atomic size and alignment is the same as the data",
10+
"[atomic]", bool, char, signed char, unsigned char,
11+
short int, unsigned short int, int, unsigned int, long int,
12+
unsigned long int) {
13+
static_assert(sizeof(stdx::atomic<TestType>) == sizeof(TestType));
14+
static_assert(alignof(stdx::atomic<TestType>) == alignof(TestType));
15+
}
16+
17+
TEMPLATE_TEST_CASE("atomic is default constructible when data is", "[atomic]",
18+
bool, char, signed char, unsigned char, short int,
19+
unsigned short int, int, unsigned int, long int,
20+
unsigned long int) {
21+
static_assert(std::is_default_constructible_v<stdx::atomic<TestType>>);
22+
}
23+
24+
namespace {
25+
struct non_dc {
26+
non_dc(int) {}
27+
};
28+
} // namespace
29+
30+
#if __cplusplus >= 202002L
31+
TEST_CASE("atomic is not default constructible when data is not", "[atomic]") {
32+
static_assert(not std::is_default_constructible_v<stdx::atomic<non_dc>>);
33+
}
34+
#endif
35+
36+
TEST_CASE("atomic is not copyable or movable", "[atomic]") {
37+
static_assert(not std::is_copy_constructible_v<stdx::atomic<int>>);
38+
static_assert(not std::is_move_constructible_v<stdx::atomic<int>>);
39+
static_assert(not std::is_copy_assignable_v<stdx::atomic<int>>);
40+
static_assert(not std::is_move_assignable_v<stdx::atomic<int>>);
41+
}
42+
43+
TEMPLATE_TEST_CASE("atomic supports value initialization", "[atomic]", bool,
44+
char, signed char, unsigned char, short int,
45+
unsigned short int, int, unsigned int, long int,
46+
unsigned long int) {
47+
static_assert(std::is_constructible_v<stdx::atomic<TestType>, TestType>);
48+
[[maybe_unused]] auto x = stdx::atomic<TestType>{TestType{}};
49+
}
50+
51+
TEST_CASE("load", "[atomic]") {
52+
stdx::atomic<std::uint32_t> val{17};
53+
CHECK(val.load() == 17);
54+
}
55+
56+
TEST_CASE("store", "[atomic]") {
57+
stdx::atomic<std::uint32_t> val{17};
58+
val.store(1337);
59+
CHECK(val.load() == 1337);
60+
}
61+
62+
TEST_CASE("implicit conversion to T", "[atomic]") {
63+
stdx::atomic<std::uint32_t> val{17};
64+
CHECK(val == 17);
65+
}
66+
67+
TEST_CASE("assignment from T", "[atomic]") {
68+
stdx::atomic<std::uint32_t> val{17};
69+
val = 1337;
70+
CHECK(val == 1337);
71+
}
72+
73+
TEST_CASE("exchange", "[atomic]") {
74+
stdx::atomic<std::uint32_t> val{17};
75+
CHECK(val.exchange(1337) == 17);
76+
CHECK(val.load() == 1337);
77+
}
78+
79+
TEST_CASE("fetch_add", "[atomic]") {
80+
stdx::atomic<std::uint32_t> val{17};
81+
CHECK(val.fetch_add(42) == 17);
82+
CHECK(val.load() == 59);
83+
}
84+
85+
TEST_CASE("fetch_sub", "[atomic]") {
86+
stdx::atomic<std::uint32_t> val{59};
87+
CHECK(val.fetch_sub(42) == 59);
88+
CHECK(val.load() == 17);
89+
}
90+
91+
TEST_CASE("operator +=", "[atomic]") {
92+
stdx::atomic<std::uint32_t> val{17};
93+
CHECK((val += 42) == 59);
94+
CHECK(val.load() == 59);
95+
}
96+
97+
TEST_CASE("operator -=", "[atomic]") {
98+
stdx::atomic<std::uint32_t> val{59};
99+
CHECK((val -= 42) == 17);
100+
CHECK(val.load() == 17);
101+
}
102+
103+
TEST_CASE("pre-increment", "[atomic]") {
104+
stdx::atomic<std::uint32_t> val{17};
105+
CHECK(++val == 18);
106+
CHECK(val.load() == 18);
107+
}
108+
109+
TEST_CASE("post-increment", "[atomic]") {
110+
stdx::atomic<std::uint32_t> val{17};
111+
CHECK(val++ == 17);
112+
CHECK(val.load() == 18);
113+
}
114+
115+
TEST_CASE("pre-decrement", "[atomic]") {
116+
stdx::atomic<std::uint32_t> val{17};
117+
CHECK(--val == 16);
118+
CHECK(val.load() == 16);
119+
}
120+
121+
TEST_CASE("post-decrement", "[atomic]") {
122+
stdx::atomic<std::uint32_t> val{17};
123+
CHECK(val-- == 17);
124+
CHECK(val.load() == 16);
125+
}
126+
127+
TEST_CASE("fetch_and", "[atomic]") {
128+
stdx::atomic<std::uint32_t> val{0b101};
129+
CHECK(val.fetch_and(0b100) == 0b101);
130+
CHECK(val.load() == 0b100);
131+
}
132+
133+
TEST_CASE("fetch_or", "[atomic]") {
134+
stdx::atomic<std::uint32_t> val{0b1};
135+
CHECK(val.fetch_or(0b100) == 0b1);
136+
CHECK(val.load() == 0b101);
137+
}
138+
139+
TEST_CASE("fetch_xor", "[atomic]") {
140+
stdx::atomic<std::uint32_t> val{0b101};
141+
CHECK(val.fetch_xor(0b1) == 0b101);
142+
CHECK(val.load() == 0b100);
143+
}
144+
145+
TEST_CASE("operator &=", "[atomic]") {
146+
stdx::atomic<std::uint32_t> val{0b101};
147+
CHECK((val &= 0b100) == 0b100);
148+
CHECK(val.load() == 0b100);
149+
}
150+
151+
TEST_CASE("operator |=", "[atomic]") {
152+
stdx::atomic<std::uint32_t> val{0b1};
153+
CHECK((val |= 0b100) == 0b101);
154+
CHECK(val.load() == 0b101);
155+
}
156+
157+
TEST_CASE("operator ^=", "[atomic]") {
158+
stdx::atomic<std::uint32_t> val{0b101};
159+
CHECK((val ^= 0b1) == 0b100);
160+
CHECK(val.load() == 0b100);
161+
}

0 commit comments

Comments
 (0)