-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #159 from elbeno/atomic
✨ Add `atomic`
- Loading branch information
Showing
13 changed files
with
387 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
|
||
== `atomic.hpp` | ||
|
||
https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic.hpp[`atomic.hpp`] | ||
provides an implementation of | ||
https://en.cppreference.com/w/cpp/atomic/atomic[`std::atomic`] with a few | ||
differences. | ||
|
||
`stdx::atomic` does not implement: | ||
|
||
* `is_lock_free` or `is_always_lock_free` | ||
* `compare_exchange_{weak,strong}` | ||
* `wait` | ||
* `notify_{one,all}` | ||
* `fetch_{max,min}` | ||
|
||
However, `stdx::atomic` allows customization of the atomic implementation for | ||
best codegen. `stdx::atomic` is implemented using the atomic API exposed by | ||
Intel's https://github.com/intel/cpp-baremetal-concurrency[baremetal concurrency | ||
library]. | ||
|
||
For example, it is possible that a particular platform requires atomic accesses | ||
to be 32-bit aligned. To achieve that for `stdx::atomic<bool>`, we could provide a | ||
configuration header specializing `::atomic::alignment_of`: | ||
|
||
[source,cpp] | ||
---- | ||
// this header: atomic_cfg.hpp | ||
#include <cstdint> | ||
template <> | ||
constexpr inline auto ::atomic::alignment_of<bool> = alignof(std::uint32_t); | ||
---- | ||
|
||
To apply this configuration, when compiling, pass `-DATOMIC_CFG="<path>/atomic_cfg.hpp"`. | ||
The result would be that `stdx::atomic<bool>` has 32-bit alignment: | ||
|
||
[source,cpp] | ||
---- | ||
static_assert(alignof(stdx::atomic<bool>) == alignof(std::uint32_t)); | ||
---- | ||
|
||
Using the https://github.com/intel/cpp-baremetal-concurrency[baremetal | ||
concurrency library] it is possible to override the handling of atomic access | ||
(`load`, `store`, `exchange`, `fetch_<op>`) to ensure the best codegen on a | ||
particular platform. As well as alignment concerns, for instance it may be the | ||
case on a single-core microcontroller that it is cheaper to disable and | ||
re-enable interrupts around a read/write than incurring a lock-free atomic | ||
access. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
#pragma once | ||
|
||
#include <conc/atomic.hpp> | ||
|
||
#include <atomic> | ||
#include <type_traits> | ||
|
||
#if __cplusplus >= 202002L | ||
#define CPP20(...) __VA_ARGS__ | ||
#else | ||
#define CPP20(...) | ||
#endif | ||
|
||
namespace stdx { | ||
inline namespace v1 { | ||
template <typename T> class atomic { | ||
static_assert(std::is_trivially_copyable_v<T> and | ||
std::is_copy_constructible_v<T> and | ||
std::is_move_constructible_v<T> and | ||
std::is_copy_assignable_v<T> and | ||
std::is_move_assignable_v<T>, | ||
"Atomic values must be trivially copyable, copy " | ||
"constructible and copy assignable"); | ||
|
||
using elem_t = ::atomic::atomic_type_t<T>; | ||
constexpr static auto alignment = ::atomic::alignment_of<T>; | ||
|
||
static_assert(std::is_convertible_v<elem_t, T>, | ||
"::atomic::atomic_type_t specialization result must be " | ||
"convertible to T"); | ||
static_assert(std::is_convertible_v<T, elem_t>, | ||
"::atomic::atomic_type_t specialization result must be " | ||
"convertible from T"); | ||
|
||
alignas(alignment) elem_t value; | ||
|
||
public: | ||
using value_type = T; | ||
|
||
constexpr atomic() CPP20(requires std::is_default_constructible_v<elem_t>) | ||
: value{} {} | ||
constexpr atomic(T t) : value{static_cast<elem_t>(t)} {} | ||
atomic(atomic const &) = delete; | ||
auto operator=(atomic const &) -> atomic & = delete; | ||
|
||
[[nodiscard]] auto | ||
load(std::memory_order mo = std::memory_order_seq_cst) const -> T { | ||
return static_cast<T>(::atomic::load(value, mo)); | ||
} | ||
|
||
void store(T t, std::memory_order mo = std::memory_order_seq_cst) { | ||
::atomic::store(value, static_cast<elem_t>(t), mo); | ||
} | ||
|
||
[[nodiscard]] operator T() const { return load(); } | ||
auto operator=(T t) -> T { | ||
store(t); | ||
return t; | ||
} | ||
|
||
[[nodiscard]] auto | ||
exchange(T t, std::memory_order mo = std::memory_order_seq_cst) -> T { | ||
return ::atomic::exchange(value, static_cast<elem_t>(t), mo); | ||
} | ||
|
||
auto fetch_add(T t, std::memory_order mo = std::memory_order_seq_cst) -> T { | ||
CPP20(static_assert( | ||
requires { t + t; }, "T must support operator+(x, y)")); | ||
return ::atomic::fetch_add(value, static_cast<elem_t>(t), mo); | ||
} | ||
auto fetch_sub(T t, std::memory_order mo = std::memory_order_seq_cst) -> T { | ||
CPP20(static_assert( | ||
requires { t - t; }, "T must support operator-(x, y)")); | ||
return ::atomic::fetch_sub(value, static_cast<elem_t>(t), mo); | ||
} | ||
|
||
auto operator+=(T t) -> T { return fetch_add(t) + t; } | ||
auto operator-=(T t) -> T { return fetch_sub(t) - t; } | ||
|
||
auto operator++() -> T { | ||
CPP20(static_assert( | ||
requires(T t) { ++t; }, "T must support operator++()")); | ||
return ::atomic::fetch_add(value, 1) + 1; | ||
} | ||
[[nodiscard]] auto operator++(int) -> T { | ||
CPP20(static_assert( | ||
requires(T t) { t++; }, "T must support operator++(int)")); | ||
return ::atomic::fetch_add(value, 1); | ||
} | ||
auto operator--() -> T { | ||
CPP20(static_assert( | ||
requires(T t) { --t; }, "T must support operator--()")); | ||
return ::atomic::fetch_sub(value, 1) - 1; | ||
} | ||
[[nodiscard]] auto operator--(int) -> T { | ||
CPP20(static_assert( | ||
requires(T t) { t--; }, "T must support operator--(int)")); | ||
return ::atomic::fetch_sub(value, 1); | ||
} | ||
|
||
auto fetch_and(T t, std::memory_order mo = std::memory_order_seq_cst) -> T { | ||
CPP20(static_assert( | ||
requires { t & t; }, "T must support operator&(x, y)")); | ||
return ::atomic::fetch_and(value, static_cast<elem_t>(t), mo); | ||
} | ||
auto fetch_or(T t, std::memory_order mo = std::memory_order_seq_cst) -> T { | ||
CPP20(static_assert( | ||
requires { t | t; }, "T must support operator|(x, y)")); | ||
return ::atomic::fetch_or(value, static_cast<elem_t>(t), mo); | ||
} | ||
auto fetch_xor(T t, std::memory_order mo = std::memory_order_seq_cst) -> T { | ||
CPP20(static_assert( | ||
requires { t ^ t; }, "T must support operator^(x, y)")); | ||
return ::atomic::fetch_xor(value, static_cast<elem_t>(t), mo); | ||
} | ||
|
||
auto operator&=(T t) -> T { return fetch_and(t) & t; } | ||
auto operator|=(T t) -> T { return fetch_or(t) | t; } | ||
auto operator^=(T t) -> T { return fetch_xor(t) ^ t; } | ||
}; | ||
} // namespace v1 | ||
} // namespace stdx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
#include <stdx/atomic.hpp> | ||
|
||
#include <catch2/catch_template_test_macros.hpp> | ||
#include <catch2/catch_test_macros.hpp> | ||
|
||
#include <cstdint> | ||
#include <type_traits> | ||
|
||
TEMPLATE_TEST_CASE("atomic size and alignment is the same as the data", | ||
"[atomic]", bool, char, signed char, unsigned char, | ||
short int, unsigned short int, int, unsigned int, long int, | ||
unsigned long int) { | ||
static_assert(sizeof(stdx::atomic<TestType>) == sizeof(TestType)); | ||
static_assert(alignof(stdx::atomic<TestType>) == alignof(TestType)); | ||
} | ||
|
||
TEMPLATE_TEST_CASE("atomic is default constructible when data is", "[atomic]", | ||
bool, char, signed char, unsigned char, short int, | ||
unsigned short int, int, unsigned int, long int, | ||
unsigned long int) { | ||
static_assert(std::is_default_constructible_v<stdx::atomic<TestType>>); | ||
} | ||
|
||
namespace { | ||
struct non_dc { | ||
non_dc(int) {} | ||
}; | ||
} // namespace | ||
|
||
#if __cplusplus >= 202002L | ||
TEST_CASE("atomic is not default constructible when data is not", "[atomic]") { | ||
static_assert(not std::is_default_constructible_v<stdx::atomic<non_dc>>); | ||
} | ||
#endif | ||
|
||
TEST_CASE("atomic is not copyable or movable", "[atomic]") { | ||
static_assert(not std::is_copy_constructible_v<stdx::atomic<int>>); | ||
static_assert(not std::is_move_constructible_v<stdx::atomic<int>>); | ||
static_assert(not std::is_copy_assignable_v<stdx::atomic<int>>); | ||
static_assert(not std::is_move_assignable_v<stdx::atomic<int>>); | ||
} | ||
|
||
TEMPLATE_TEST_CASE("atomic supports value initialization", "[atomic]", bool, | ||
char, signed char, unsigned char, short int, | ||
unsigned short int, int, unsigned int, long int, | ||
unsigned long int) { | ||
static_assert(std::is_constructible_v<stdx::atomic<TestType>, TestType>); | ||
[[maybe_unused]] auto x = stdx::atomic<TestType>{TestType{}}; | ||
} | ||
|
||
TEST_CASE("load", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(val.load() == 17); | ||
} | ||
|
||
TEST_CASE("store", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
val.store(1337); | ||
CHECK(val.load() == 1337); | ||
} | ||
|
||
TEST_CASE("implicit conversion to T", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(val == 17); | ||
} | ||
|
||
TEST_CASE("assignment from T", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
val = 1337; | ||
CHECK(val == 1337); | ||
} | ||
|
||
TEST_CASE("exchange", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(val.exchange(1337) == 17); | ||
CHECK(val.load() == 1337); | ||
} | ||
|
||
TEST_CASE("fetch_add", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(val.fetch_add(42) == 17); | ||
CHECK(val.load() == 59); | ||
} | ||
|
||
TEST_CASE("fetch_sub", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{59}; | ||
CHECK(val.fetch_sub(42) == 59); | ||
CHECK(val.load() == 17); | ||
} | ||
|
||
TEST_CASE("operator +=", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK((val += 42) == 59); | ||
CHECK(val.load() == 59); | ||
} | ||
|
||
TEST_CASE("operator -=", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{59}; | ||
CHECK((val -= 42) == 17); | ||
CHECK(val.load() == 17); | ||
} | ||
|
||
TEST_CASE("pre-increment", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(++val == 18); | ||
CHECK(val.load() == 18); | ||
} | ||
|
||
TEST_CASE("post-increment", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(val++ == 17); | ||
CHECK(val.load() == 18); | ||
} | ||
|
||
TEST_CASE("pre-decrement", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(--val == 16); | ||
CHECK(val.load() == 16); | ||
} | ||
|
||
TEST_CASE("post-decrement", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{17}; | ||
CHECK(val-- == 17); | ||
CHECK(val.load() == 16); | ||
} | ||
|
||
TEST_CASE("fetch_and", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{0b101}; | ||
CHECK(val.fetch_and(0b100) == 0b101); | ||
CHECK(val.load() == 0b100); | ||
} | ||
|
||
TEST_CASE("fetch_or", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{0b1}; | ||
CHECK(val.fetch_or(0b100) == 0b1); | ||
CHECK(val.load() == 0b101); | ||
} | ||
|
||
TEST_CASE("fetch_xor", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{0b101}; | ||
CHECK(val.fetch_xor(0b1) == 0b101); | ||
CHECK(val.load() == 0b100); | ||
} | ||
|
||
TEST_CASE("operator &=", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{0b101}; | ||
CHECK((val &= 0b100) == 0b100); | ||
CHECK(val.load() == 0b100); | ||
} | ||
|
||
TEST_CASE("operator |=", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{0b1}; | ||
CHECK((val |= 0b100) == 0b101); | ||
CHECK(val.load() == 0b101); | ||
} | ||
|
||
TEST_CASE("operator ^=", "[atomic]") { | ||
stdx::atomic<std::uint32_t> val{0b101}; | ||
CHECK((val ^= 0b1) == 0b100); | ||
CHECK(val.load() == 0b100); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.