Skip to content

Commit

Permalink
Additions to the atomics library.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jhuighuy committed Feb 16, 2025
1 parent 1331ff3 commit 919a5d4
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 4 deletions.
76 changes: 72 additions & 4 deletions source/tit/core/par/atomic.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,89 @@
#pragma once

#include <concepts>
#include <thread>
#include <type_traits>
#include <utility>

#include "tit/core/basic_types.hpp"
#include "tit/core/type_utils.hpp"

namespace tit::par {

// NOLINTBEGIN(*-pro-type-vararg)

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/// Atomically perform addition and return what was stored before.
/// Atomic type.
template<class Val>
requires std::integral<Val> || std::is_pointer_v<Val>
auto fetch_and_add(Val& val, difference_t<Val> delta) noexcept -> Val {
return __atomic_fetch_add(&val, delta, __ATOMIC_RELAXED); // NOLINT(*-vararg)
concept atomic = std::integral<Val> || std::is_pointer_v<Val>;

/// Memory order.
enum class MemOrder : uint8_t {
relaxed = __ATOMIC_RELAXED, ///< Relaxed memory order.
acquire = __ATOMIC_ACQUIRE, ///< Acquire memory order.
release = __ATOMIC_RELEASE, ///< Release memory order.
acq_rel = __ATOMIC_ACQ_REL, ///< Acquire-release memory order.
seq_cst = __ATOMIC_SEQ_CST, ///< Sequentially consistent memory order.
};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/// Atomically load the value.
template<MemOrder Order = MemOrder::acquire, atomic Val>
[[gnu::always_inline]]
inline auto load(const Val& val) noexcept -> Val {
return __atomic_load_n(&val, std::to_underlying(Order));
}

/// Atomically store the value.
template<MemOrder Order = MemOrder::release, atomic Val>
[[gnu::always_inline]]
inline void store(Val& val, Val desired) noexcept {
__atomic_store_n(&val, desired, std::to_underlying(Order));
}

/// Atomically wait for the value to change.
template<MemOrder Order = MemOrder::acquire, atomic Val>
[[gnu::always_inline]]
inline auto wait(const Val& val, Val old) noexcept -> Val {
while (true) {
if (const auto current = load<Order>(val); current != old) return current;
std::this_thread::yield();
}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/// Atomically compare and exchange the value.
template<MemOrder SuccessOrder = MemOrder::relaxed,
MemOrder FailureOrder = MemOrder::relaxed,
atomic Val>
[[gnu::always_inline]]
inline auto compare_exchange(Val& val,
std::type_identity_t<Val> expected,
std::type_identity_t<Val> desired) noexcept
-> bool {
return __atomic_compare_exchange_n( //
&val,
&expected,
desired,
/*weak=*/false,
std::to_underlying(SuccessOrder),
std::to_underlying(FailureOrder));
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/// Atomically perform addition and return what was stored before.
template<MemOrder Order = MemOrder::relaxed, atomic Val>
[[gnu::always_inline]]
inline auto fetch_and_add(Val& val, difference_t<Val> delta) noexcept -> Val {
return __atomic_fetch_add(&val, delta, std::to_underlying(Order));
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// NOLINTEND(*-pro-type-vararg)

} // namespace tit::par
58 changes: 58 additions & 0 deletions source/tit/core/par/atomic.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,71 @@
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

#include <chrono>
#include <ranges>
#include <thread>

#include "tit/core/par/algorithms.hpp"
#include "tit/core/par/atomic.hpp"
#include "tit/core/par/control.hpp"

#include "tit/testing/test.hpp"

namespace tit {
namespace {

// Disclaimer: Since this submodule is no more that a simple wrapper around the
// GCC atomics intrinsics, there is no need to test it in detail. The only thing
// we need to test is that our wrappers are working correctly.

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

TEST_CASE("par::load") {
const auto val = 10;
CHECK(par::load(val) == val);
}

TEST_CASE("par::store") {
auto val = 10;
constexpr auto desired = 20;
par::store(val, desired);
CHECK(val == desired);
}

TEST_CASE("par::wait") {
static constexpr auto init = 10;
static constexpr auto updated = 200;
auto val = init;
par::set_num_threads(4);
par::for_each(std::views::iota(0, 4), [&val](int i) {
if (i == 2) {
std::this_thread::sleep_for(std::chrono::milliseconds{10});
par::store(val, updated);
} else {
CHECK(par::wait(val, init) == updated);
}
});
CHECK(val == updated);
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

TEST_CASE("par::compare_exchange") {
constexpr auto expected = 10;
constexpr auto desired = 20;
SUBCASE("success") {
auto val = expected;
CHECK(par::compare_exchange(val, expected, desired));
CHECK(val == desired);
}
SUBCASE("failure") {
constexpr auto unexpected = 30;
auto val = unexpected;
CHECK_FALSE(par::compare_exchange(val, expected, desired));
CHECK(val == unexpected);
}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

TEST_CASE("par::fetch_and_add") {
Expand Down

0 comments on commit 919a5d4

Please sign in to comment.