Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.xmake
build/
build-*/
out/

# xlings
.xlings
Expand Down Expand Up @@ -50,4 +51,4 @@ Makefile

# IDE
/.idea
/.cache
/.cache
4 changes: 2 additions & 2 deletions src/operations/dispatcher.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ constexpr auto dispatch(Lhs const &lhs, Rhs const &rhs)

// Runtime stage 2: value path.
auto const lhs_rep_raw =
underlying::traits<typename meta::lhs_value_type>::to_rep(lhs.value());
underlying::traits<typename meta::lhs_value_type>::to_rep(lhs.load());
auto const rhs_rep_raw =
underlying::traits<typename meta::rhs_value_type>::to_rep(rhs.value());
underlying::traits<typename meta::rhs_value_type>::to_rep(rhs.load());
Comment thread
FrozenLemonTee marked this conversation as resolved.

if (!underlying::traits<typename meta::lhs_value_type>::is_valid_rep(
lhs_rep_raw) ||
Expand Down
27 changes: 27 additions & 0 deletions src/policy/impl.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,33 @@ struct concurrency::handler<concurrency::none, OpTag, CommonRep, ErrorPayload> {
}
};

template <typename CommonRep, typename ErrorPayload>
struct concurrency::handler<concurrency::none, void, CommonRep, ErrorPayload> {
static constexpr bool enabled = true;
using injection_type = concurrency::injection;
using result_type = std::expected<CommonRep, ErrorPayload>;

static constexpr auto load(CommonRep const &value) noexcept -> CommonRep {
return value;
}

static constexpr auto store(CommonRep &value, CommonRep desired) noexcept
-> void {
value = desired;
}

static constexpr auto compare_exchange(CommonRep &value, CommonRep &expected,
CommonRep desired) noexcept -> bool {
if (value != expected) {
expected = value;
return false;
}

value = desired;
return true;
}
Comment thread
FrozenLemonTee marked this conversation as resolved.
};

template <operations::operation OpTag, typename CommonRep,
typename ErrorPayload>
struct concurrency::handler<concurrency::fenced, OpTag, CommonRep,
Expand Down
53 changes: 32 additions & 21 deletions src/primitive/impl.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -58,50 +58,61 @@ public:
"Multiple concurrency policies are not allowed");

constexpr explicit primitive(value_type v) noexcept : value_(v) {}
primitive(primitive const &other) noexcept : value_(other.load()) {}
auto operator=(primitive const &other) noexcept -> primitive & {
if (this == &other) {
return *this;
}

store(other.load());
return *this;
}

primitive(primitive &&other) noexcept : value_(other.load()) {}
auto operator=(primitive &&other) noexcept -> primitive & {
if (this == &other) {
return *this;
}

store(other.load());
Comment thread
FrozenLemonTee marked this conversation as resolved.
Outdated
return *this;
}

constexpr value_type &value() noexcept { return value_; }
[[nodiscard]] constexpr value_type const &value() const noexcept {
return value_;
}
constexpr explicit operator value_type() const noexcept { return value_; }

[[nodiscard]] auto load() const noexcept -> value_type {
using access_handler_t =
policy::concurrency::handler<concurrency_policy, void, value_type,
policy::error::kind>;
static_assert(
policy::concurrency::handler_access_available<concurrency_policy,
value_type>,
"Selected concurrency policy does not provide primitive "
"load/store/CAS support");
require_access_handler_();
return access_handler_t::load(value_);
}

auto store(value_type desired) noexcept -> void {
using access_handler_t =
policy::concurrency::handler<concurrency_policy, void, value_type,
policy::error::kind>;
static_assert(
policy::concurrency::handler_access_available<concurrency_policy,
value_type>,
"Selected concurrency policy does not provide primitive "
"load/store/CAS support");
require_access_handler_();
access_handler_t::store(value_, desired);
}

auto compare_exchange(value_type &expected, value_type desired) noexcept
-> bool {
using access_handler_t =
policy::concurrency::handler<concurrency_policy, void, value_type,
policy::error::kind>;
require_access_handler_();
return access_handler_t::compare_exchange(value_, expected, desired);
}

private:
using access_handler_t =
policy::concurrency::handler<concurrency_policy, void, value_type,
policy::error::kind>;

static constexpr auto require_access_handler_() noexcept -> void {
static_assert(
policy::concurrency::handler_access_available<concurrency_policy,
value_type>,
"Selected concurrency policy does not provide primitive "
"load/store/CAS support");
return access_handler_t::compare_exchange(value_, expected, desired);
}

private:
value_type value_;
};

Expand Down
117 changes: 117 additions & 0 deletions tests/basic/test_operations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <gtest/gtest.h>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>

import mcpplibs.primitives;
Expand Down Expand Up @@ -196,6 +197,122 @@ TEST(OperationsTest, PrimitiveFencedCasSupportsConcurrentIncrements) {
EXPECT_EQ(counter.load(), kThreadCount * kIterationsPerThread);
}

TEST(OperationsTest,
BinaryOperationsWithLoadStoreRemainStableUnderHighConcurrency) {
using value_t =
primitive<int, policy::value::checked, policy::concurrency::fenced,
policy::error::expected>;

constexpr int kWriterThreads = 6;
constexpr int kReaderThreads = 8;
constexpr int kIterationsPerThread = 25000;
constexpr int kMaxOperand = 100000;

auto lhs = value_t{0};
auto rhs = value_t{0};
auto sink = value_t{0};

std::atomic<int> add_error_count{0};
std::atomic<int> sub_error_count{0};
std::atomic<int> range_violation_count{0};
std::atomic<bool> start{false};

std::vector<std::thread> workers;
workers.reserve(kWriterThreads + kReaderThreads);

for (int writer = 0; writer < kWriterThreads; ++writer) {
workers.emplace_back([&, writer]() {
while (!start.load(std::memory_order_acquire)) {
}

for (int n = 0; n < kIterationsPerThread; ++n) {
auto const v1 = (writer + n) % (kMaxOperand + 1);
auto const v2 = (writer * 3 + n * 7) % (kMaxOperand + 1);
lhs.store(v1);
rhs.store(v2);
}
});
}

for (int reader = 0; reader < kReaderThreads; ++reader) {
workers.emplace_back([&, reader]() {
while (!start.load(std::memory_order_acquire)) {
}

for (int n = 0; n < kIterationsPerThread; ++n) {
if (((reader + n) & 1) == 0) {
auto const out = operations::add(lhs, rhs);
if (!out.has_value()) {
add_error_count.fetch_add(1, std::memory_order_relaxed);
continue;
}

auto const v = out->load();
if (v < 0 || v > (kMaxOperand * 2)) {
range_violation_count.fetch_add(1, std::memory_order_relaxed);
}
sink.store(v);
auto const snapshot = sink.load();
if (snapshot < -kMaxOperand || snapshot > (kMaxOperand * 2)) {
range_violation_count.fetch_add(1, std::memory_order_relaxed);
}
continue;
}

auto const out = operations::sub(lhs, rhs);
if (!out.has_value()) {
sub_error_count.fetch_add(1, std::memory_order_relaxed);
continue;
}

auto const v = out->load();
if (v < -kMaxOperand || v > kMaxOperand) {
range_violation_count.fetch_add(1, std::memory_order_relaxed);
}
sink.store(v);
auto const snapshot = sink.load();
if (snapshot < -kMaxOperand || snapshot > (kMaxOperand * 2)) {
range_violation_count.fetch_add(1, std::memory_order_relaxed);
}
}
});
}

start.store(true, std::memory_order_release);

for (auto &worker : workers) {
worker.join();
}

EXPECT_EQ(add_error_count.load(std::memory_order_relaxed), 0);
EXPECT_EQ(sub_error_count.load(std::memory_order_relaxed), 0);
EXPECT_EQ(range_violation_count.load(std::memory_order_relaxed), 0);
}

TEST(OperationsTest, PrimitiveSupportsCopyAndMoveSpecialMembers) {
using value_t = primitive<int, policy::value::checked, policy::error::expected>;

static_assert(std::is_copy_constructible_v<value_t>);
static_assert(std::is_copy_assignable_v<value_t>);
static_assert(std::is_move_constructible_v<value_t>);
static_assert(std::is_move_assignable_v<value_t>);

auto original = value_t{42};
auto copy_constructed = value_t{original};
EXPECT_EQ(copy_constructed.load(), 42);

auto copy_assigned = value_t{0};
copy_assigned = original;
EXPECT_EQ(copy_assigned.load(), 42);

auto move_constructed = value_t{std::move(copy_assigned)};
EXPECT_EQ(move_constructed.load(), 42);

auto move_assigned = value_t{0};
move_assigned = std::move(move_constructed);
EXPECT_EQ(move_assigned.load(), 42);
}

TEST(OperationsTest, StrictTypeRejectsMixedTypesAtCompileTime) {
using lhs_t = primitive<int, policy::value::checked, policy::type::strict,
policy::error::expected>;
Expand Down
6 changes: 3 additions & 3 deletions tests/basic/test_policies.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ TEST(PolicyConcurrencyTest, FencedVariantsUseExpectedMemoryOrders) {
}

TEST(PolicyConcurrencyTest, PrimitiveAccessHandlerProtocolByPolicy) {
EXPECT_TRUE(
(policy::concurrency::handler_access_available<policy::concurrency::none,
int>));
EXPECT_TRUE((
policy::concurrency::handler_access_available<policy::concurrency::fenced,
int>));
Expand All @@ -131,9 +134,6 @@ TEST(PolicyConcurrencyTest, PrimitiveAccessHandlerProtocolByPolicy) {
policy::concurrency::fenced_acq_rel, int>));
EXPECT_TRUE((policy::concurrency::handler_access_available<
policy::concurrency::fenced_seq_cst, int>));
EXPECT_FALSE(
(policy::concurrency::handler_access_available<policy::concurrency::none,
int>));
}

TEST(PolicyConcurrencyTest, PrimitiveAccessRejectsNonTriviallyCopyableRep) {
Expand Down
Loading