Skip to content

Commit

Permalink
Merge pull request #159 from elbeno/atomic
Browse files Browse the repository at this point in the history
✨ Add `atomic`
  • Loading branch information
elbeno authored Oct 16, 2024
2 parents 99231b9 + 9d8de7f commit a36370b
Show file tree
Hide file tree
Showing 13 changed files with 387 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ endif()

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

if(NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 20)
Expand All @@ -39,6 +39,7 @@ target_sources(
include
FILES
include/stdx/algorithm.hpp
include/stdx/atomic.hpp
include/stdx/atomic_bitset.hpp
include/stdx/bit.hpp
include/stdx/bitset.hpp
Expand Down
49 changes: 49 additions & 0 deletions docs/atomic.adoc
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.
1 change: 1 addition & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:toc: left

include::intro.adoc[]
include::atomic.adoc[]
include::atomic_bitset.adoc[]
include::algorithm.adoc[]
include::bit.adoc[]
Expand Down
1 change: 1 addition & 0 deletions docs/intro.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ into headers whose names match the standard.
The following headers are available:

* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/algorithm.hpp[`algorithm.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic.hpp[`atomic.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic_bitset.hpp[`atomic_bitset.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bit.hpp[`bit.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bitset.hpp[`bitset.hpp`]
Expand Down
122 changes: 122 additions & 0 deletions include/stdx/atomic.hpp
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
6 changes: 6 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ add_tests(
FILES
algorithm
always_false
atomic
atomic_override
atomic_bitset
atomic_bitset_override
bind
Expand Down Expand Up @@ -65,6 +67,10 @@ target_compile_definitions(
atomic_bitset_override_test
PRIVATE -DATOMIC_CFG="${CMAKE_CURRENT_LIST_DIR}/detail/atomic_cfg.hpp")

target_compile_definitions(
atomic_override_test
PRIVATE -DATOMIC_CFG="${CMAKE_CURRENT_LIST_DIR}/detail/atomic_cfg.hpp")

if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20)
add_tests(FILES ct_format ct_string indexed_tuple tuple tuple_algorithms)
endif()
Expand Down
161 changes: 161 additions & 0 deletions test/atomic.cpp
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);
}
2 changes: 1 addition & 1 deletion test/atomic_bitset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ TEST_CASE("to_natural returns smallest_uint", "[atomic_bitset]") {
auto bs = stdx::atomic_bitset<4>{stdx::all_bits};
auto value = bs.to_natural();
CHECK(value == 0b1111);
static_assert(std::same_as<decltype(value), std::uint8_t>);
static_assert(std::is_same_v<decltype(value), std::uint8_t>);
}

TEMPLATE_TEST_CASE("construct with a string_view", "[atomic_bitset]",
Expand Down
Loading

0 comments on commit a36370b

Please sign in to comment.