diff --git a/CMakeSettings.json b/CMakeSettings.json index 520e8492..96283bc3 100644 --- a/CMakeSettings.json +++ b/CMakeSettings.json @@ -9,7 +9,7 @@ "cmakeCommandArgs": "-DGAPP_CXX_FLAGS=/analyze:WX- -DGAPP_BUILD_TESTS=ON -DGAPP_BUILD_BENCHMARKS=ON -DGAPP_BUILD_EXAMPLES=ON", "ctestCommandArgs": "--output-on-failure --schedule-random", "codeAnalysisRuleset": "${projectDir}\\core-guidelines.ruleset", - "enableMicrosoftCodeAnalysis": false, + "enableMicrosoftCodeAnalysis": true, "inheritEnvironments": [ "msvc_x64_x64" ] }, { diff --git a/src/utility/bit.hpp b/src/utility/bit.hpp new file mode 100644 index 00000000..ec4e8d8f --- /dev/null +++ b/src/utility/bit.hpp @@ -0,0 +1,31 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#ifndef GA_UTILITY_BIT_HPP +#define GA_UTILITY_BIT_HPP + +#include "utility.hpp" +#include + +namespace gapp::detail +{ + template + inline constexpr size_t bitsizeof = CHAR_BIT * sizeof(T); + + template + inline constexpr T lsb_mask = T{ 1 }; + + template + inline constexpr T msb_mask = T{ 1 } << (bitsizeof - 1); + + + template + constexpr bool is_nth_bit_set(T value, size_t n) noexcept + { + GAPP_ASSERT(n < bitsizeof); + + return value & (T{ 1 } << n); + } + +} // namespace gapp::detail + +#endif // !GA_UTILITY_BIT_HPP diff --git a/src/utility/rcu.hpp b/src/utility/rcu.hpp new file mode 100644 index 00000000..1dc64130 --- /dev/null +++ b/src/utility/rcu.hpp @@ -0,0 +1,120 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#ifndef GA_UTILITY_RCU_HPP +#define GA_UTILITY_RCU_HPP + +#include "utility.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace gapp::detail +{ + struct default_rcu_domain_tag {}; + + template + class rcu_domain + { + public: + inline static void read_lock() noexcept + { + reader.epoch.store(writer_epoch.load(std::memory_order_relaxed), std::memory_order_release); + std::ignore = reader.epoch.load(std::memory_order_acquire); + } + + inline static void read_unlock() noexcept + { + reader.epoch.store(NOT_READING, std::memory_order_release); + } + + inline static void synchronize() noexcept + { + uint64_t current = writer_epoch.load(std::memory_order_acquire); + uint64_t target = current + 1; + writer_epoch.compare_exchange_strong(current, target, std::memory_order_acq_rel); + + std::shared_lock _{ reader_list_mtx }; + + for (const registered_reader* reader_ : reader_list) + { + while (reader_->epoch.load(std::memory_order_acquire) < target) { GAPP_PAUSE(); } + } + } + + private: + struct registered_reader + { + registered_reader() noexcept + { + std::unique_lock _{ reader_list_mtx }; + reader_list.push_back(this); + } + + ~registered_reader() noexcept + { + std::unique_lock _{ reader_list_mtx }; + std::erase(reader_list, this); + } + + std::atomic epoch = NOT_READING; + }; + + inline static constexpr uint64_t NOT_READING = std::numeric_limits::max(); + + GAPP_API inline static std::vector reader_list; + GAPP_API inline static std::shared_mutex reader_list_mtx; + + alignas(128) GAPP_API inline static constinit std::atomic writer_epoch = 0; + alignas(128) inline static thread_local registered_reader reader; + }; + + + template + class rcu_obj + { + public: + template + constexpr rcu_obj(Args&&... args) : + data_(new T(std::forward(args)...)) + {} + + ~rcu_obj() noexcept + { + T* ptr = data_.load(std::memory_order_consume); + rcu_domain::synchronize(); + delete ptr; + } + + template + rcu_obj& operator=(U&& value) + { + T* new_ptr = new T(std::forward(value)); + T* old_ptr = data_.exchange(new_ptr, std::memory_order_acq_rel); + rcu_domain::synchronize(); + delete old_ptr; + + return *this; + } + + T& get() const noexcept + { + return *data_.load(std::memory_order_consume); + } + + T& operator*() const noexcept { return get(); } + T* operator->() const noexcept { return std::addressof(get()); } + + void lock() const noexcept { rcu_domain::read_lock(); } + void unlock() const noexcept { rcu_domain::read_unlock(); } + + private: + std::atomic data_; + }; + +} // namespace gapp::detail + +#endif // !GA_UTILITY_RCU_HPP diff --git a/src/utility/rng.hpp b/src/utility/rng.hpp index ca66a9df..dabcf5e9 100644 --- a/src/utility/rng.hpp +++ b/src/utility/rng.hpp @@ -6,70 +6,237 @@ #include "utility.hpp" #include "type_traits.hpp" #include "concepts.hpp" +#include "bit.hpp" +#include "rcu.hpp" #include +#include +#include #include #include #include +#include #include #include +#include +#include #include #include #include -#include + #ifndef GAPP_SEED # define GAPP_SEED 0x3da99432ab975d26LL #endif + /** Contains the PRNG classes and functions used for generating random numbers. */ namespace gapp::rng { /** - * Splitmix64 pseudo-random number generator based on https://prng.di.unimi.it/splitmix64.c \n - * All of the member functions are thread-safe. + * Splitmix64 pseudo-random number generator based on + * https://prng.di.unimi.it/splitmix64.c. This generator + * is only used for seeding the Xoroshiro generators. */ - class AtomicSplitmix64 + class Splitmix64 { public: using result_type = std::uint64_t; /**< The generator generates 64 bit integers. */ using state_type = std::uint64_t; /**< The generator has a 64 bit state. */ /** - * Create a new generator initialized from a 64 bit seed value. + * Create a new Splitmix64 generator initialized from a 64 bit seed value. * - * Instead of creating new instances of this generator, the global prng - * instance should be used. + * @note + * The global prng instance should be used instead of creating + * new instances of this generator. * * @param seed The seed used to initialize the state of the generator. */ - explicit constexpr AtomicSplitmix64(state_type seed) noexcept : + explicit constexpr Splitmix64(state_type seed) noexcept : state_(seed) {} - /** Generate the next pseudo-random number of the sequence. Thread-safe. */ - result_type operator()() noexcept; + /** @returns The next number of the sequence. */ + constexpr result_type operator()() noexcept + { + result_type z = (state_ += 0x9e3779b97f4a7c15); + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; + z = (z ^ (z >> 27)) * 0x94d049bb133111eb; + + return z ^ (z >> 31); + } + + /** Set a new seed for the generator. */ + constexpr void seed(state_type seed) noexcept { state_ = seed; } + + /** @returns The smallest possible value that can be generated. */ + static constexpr result_type min() noexcept { return std::numeric_limits::min(); } + + /** @returns The largest possible value that can be generated. */ + static constexpr result_type max() noexcept { return std::numeric_limits::max(); } + + /** Compare the internal state of 2 generators. @returns true if they are the same. */ + friend constexpr bool operator==(const Splitmix64&, const Splitmix64&) = default; + + private: + state_type state_; + }; + + + /** + * Xoroshiro128+ pseudo-random number generator based on + * https://prng.di.unimi.it/xoroshiro128plus.c + * + * @see + * David Blackman and Sebastiano Vigna. "Scrambled linear pseudorandom number generators." + * ACM Transactions on Mathematical Software 47, no. 4 (2021): 1-32. + */ + class Xoroshiro128p + { + public: + using result_type = std::uint64_t; /**< The generator generates 64 bit integers. */ + using state_type = std::array; /**< The generator has 128 bits of state. */ + + /** + * Create a Xoroshiro128+ generator initialized from a 64 bit seed + * value. + * + * @note + * The global prng instance should be used instead of creating + * new instances of this generator. + * + * @param seed The seed used to initialize the state of the generator. + */ + explicit constexpr Xoroshiro128p(std::uint64_t seed) noexcept : + state_(seed_sequence(seed)) + {} + + /** @returns The next number of the sequence. */ + constexpr result_type operator()() noexcept + { + const auto result = state_[0] + state_[1]; + const auto xstate = state_[0] ^ state_[1]; + + state_[0] = std::rotl(state_[0], 24) ^ xstate ^ (xstate << 16); + state_[1] = std::rotl(xstate, 37); + + return result; + } + + /** Advance the state of the generator by 2^96 steps. */ + constexpr Xoroshiro128p& jump() noexcept + { + state_type new_state{ 0, 0 }; + + for (std::uint64_t JUMP : { 0xd2a98b26625eee7bULL, 0xdddf9b1090aa7ac1ULL }) + { + for (std::size_t n = 0; n < 64; n++) + { + if (detail::is_nth_bit_set(JUMP, n)) + { + new_state[0] ^= state_[0]; + new_state[1] ^= state_[1]; + } + std::invoke(*this); + } + } + state_ = new_state; + + return *this; + } /** Set a new seed for the generator. */ - void seed(state_type seed) noexcept; + constexpr void seed(std::uint64_t seed) noexcept { state_ = seed_sequence(seed); } - /** @returns The smallest value that can be generated. */ - static constexpr result_type min() noexcept; + /** @returns The smallest possible value that can be generated. */ + static constexpr result_type min() noexcept { return std::numeric_limits::min(); } - /** @returns The largest value that can be generated. */ - static constexpr result_type max() noexcept; + /** @returns The largest possible value that can be generated. */ + static constexpr result_type max() noexcept { return std::numeric_limits::max(); } /** Compare the internal state of 2 generators. @returns True if they are the same. */ - friend constexpr bool operator==(const AtomicSplitmix64&, const AtomicSplitmix64&) = default; + friend constexpr bool operator==(const Xoroshiro128p&, const Xoroshiro128p&) = default; private: - std::atomic state_; + static constexpr state_type seed_sequence(std::uint64_t seed) noexcept + { + Splitmix64 seed_seq_gen(seed); + return { seed_seq_gen(), seed_seq_gen() }; + } + + alignas(128) state_type state_; + }; + + + /** + * The pseudo-random number generator class used in the library. + * This class is a simple wrapper around the Xoroshiro128p generator + * to make it thread-safe. + * + * @note + * The global prng instance should be used instead of creating + * new instances of this generator. + */ + class ConcurrentXoroshiro128p + { + public: + using result_type = Xoroshiro128p::result_type; + using state_type = Xoroshiro128p::state_type; + + /** @return The next number of the sequence. Thread-safe. */ + result_type operator()() const noexcept + { + std::scoped_lock _{ generator_.instance }; + return std::invoke(*generator_.instance); + } + + /** Set a new seed for the generator. Thread-safe. */ + static void seed(std::uint64_t seed) + { + std::scoped_lock _{ generator_list_mtx_ }; + global_generator.seed(seed); + for (Generator* generator : generator_list) + { + *generator = global_generator.jump(); + } + } + + /** @returns The smallest possible value that can be generated. */ + static constexpr result_type min() noexcept { return Xoroshiro128p::min(); } + + /** @returns The largest possible value that can be generated. */ + static constexpr result_type max() noexcept { return Xoroshiro128p::max(); } + + private: + using Generator = detail::rcu_obj; + + struct RegisteredGenerator + { + RegisteredGenerator() + { + std::scoped_lock _{ generator_list_mtx_ }; + instance = global_generator.jump(); + generator_list.push_back(std::addressof(instance)); + } + + ~RegisteredGenerator() noexcept + { + std::scoped_lock _{ generator_list_mtx_ }; + std::erase(generator_list, std::addressof(instance)); + } + + Generator instance{ 0 }; + }; + + GAPP_API inline static constinit Xoroshiro128p global_generator{ GAPP_SEED }; + GAPP_API inline static std::vector generator_list; + GAPP_API inline static std::mutex generator_list_mtx_; + alignas(128) inline static thread_local RegisteredGenerator generator_; }; - /** The pseudo-random number generator class used in the algorithms. */ - using PRNG = AtomicSplitmix64; /** The global pseudo-random number generator instance used in the algorithms. */ - inline constinit PRNG prng{ GAPP_SEED }; + inline constinit ConcurrentXoroshiro128p prng; /** Generate a random boolean value from a uniform distribution. */ @@ -77,39 +244,42 @@ namespace gapp::rng /** Generate a random integer from a uniform distribution on the closed interval [lbound, ubound]. */ template - inline IntType randomInt(IntType lbound, IntType ubound); + IntType randomInt(IntType lbound, IntType ubound); /** Generate a random floating-point value from a uniform distribution on the closed interval [0.0, 1.0]. */ template - inline RealType randomReal(); + RealType randomReal(); /** Generate a random floating-point value of from a uniform distribution on the closed interval [lbound, ubound]. */ template - inline RealType randomReal(RealType lbound, RealType ubound); + RealType randomReal(RealType lbound, RealType ubound); /** Generate a random floating-point value from a normal distribution with the specified mean and std deviation. */ template - inline RealType randomNormal(RealType mean = 0.0, RealType SD = 1.0); + RealType randomNormal(RealType mean = 0.0, RealType std_dev = 1.0); /** Generate a random integer value from a binomial distribution with the parameters n and p. */ template - inline IntType randomBinomial(IntType n, double p); + IntType randomBinomial(IntType n, double p); + /** Generate a random index for a container. */ template - inline size_t randomIdx(const T& cont); + size_t randomIdx(const T& cont); /** Pick a random element from a range. */ template - inline Iter randomElement(Iter first, Iter last); + Iter randomElement(Iter first, Iter last); + /** Generate @p count unique integers from the half-open range [@p lbound, @p ubound). */ template std::vector sampleUnique(IntType lbound, IntType ubound, size_t count); + /** Select an index based on a discrete CDF. */ template - inline size_t sampleCdf(std::span cdf); + size_t sampleCdf(std::span cdf); } // namespace gapp::rng @@ -124,36 +294,19 @@ namespace gapp::rng namespace gapp::rng { - inline AtomicSplitmix64::result_type AtomicSplitmix64::operator()() noexcept - { - result_type z = state_.fetch_add(0x9e3779b97f4a7c15, std::memory_order_acq_rel) + 0x9e3779b97f4a7c15; - z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; - z = (z ^ (z >> 27)) * 0x94d049bb133111eb; - - return z ^ (z >> 31); - } - - inline void AtomicSplitmix64::seed(state_type seed) noexcept - { - state_.store(seed, std::memory_order_release); - } - - inline constexpr AtomicSplitmix64::result_type AtomicSplitmix64::min() noexcept + bool randomBool() noexcept { - return std::numeric_limits::min(); - } + constinit thread_local uint64_t bit_pool = 1; - inline constexpr AtomicSplitmix64::result_type AtomicSplitmix64::max() noexcept - { - return std::numeric_limits::max(); - } + if (bit_pool == detail::lsb_mask) + { + bit_pool = prng() | detail::msb_mask; + } - bool randomBool() noexcept - { - static constexpr size_t nbits = CHAR_BIT * sizeof(PRNG::result_type); - static constexpr auto msb_mask = PRNG::result_type{ 1 } << (nbits - 1); + const bool bit = bit_pool & detail::lsb_mask; + bit_pool >>= 1; - return static_cast(rng::prng() & msb_mask); + return bit; } template @@ -161,15 +314,12 @@ namespace gapp::rng { GAPP_ASSERT(lbound <= ubound); - // std::uniform_int_distribution doesnt support char - if constexpr (sizeof(IntType) == 1) - { - return static_cast(std::uniform_int_distribution{ lbound, ubound }(rng::prng)); - } - else - { - return std::uniform_int_distribution{ lbound, ubound }(rng::prng); - } + // std::uniform_int_distribution doesnt support char and other 8 bit types + using NonCharInt = std::conditional_t, int64_t, uint64_t>; + + std::uniform_int_distribution dist{ lbound, ubound }; + + return dist(rng::prng); } template @@ -189,14 +339,14 @@ namespace gapp::rng } template - RealType randomNormal(RealType mean, RealType SD) + RealType randomNormal(RealType mean, RealType std_dev) { - GAPP_ASSERT(SD >= 0.0); + GAPP_ASSERT(std_dev >= 0.0); // keep the distribution for the state thread_local std::normal_distribution dist; - return SD * dist(rng::prng) + mean; + return std_dev * dist(rng::prng) + mean; } template diff --git a/src/utility/utility.hpp b/src/utility/utility.hpp index 4cafbe3b..fc0f1662 100644 --- a/src/utility/utility.hpp +++ b/src/utility/utility.hpp @@ -147,7 +147,7 @@ namespace gapp::detail return (left ^ right) >= 0; } - // returns the length of the range [low, high) + // returns the length of the range [low, high) without overflow template constexpr size_t range_length(T low, T high) noexcept { diff --git a/test/benchmark/random.cpp b/test/benchmark/random.cpp new file mode 100644 index 00000000..8d126112 --- /dev/null +++ b/test/benchmark/random.cpp @@ -0,0 +1,15 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#include +#include +#include "utility/rng.hpp" + +using namespace gapp::rng; + +TEST_CASE("prng", "[benchmark]") +{ + BENCHMARK("randomBool") { return randomBool(); }; + BENCHMARK("randomInt") { return randomInt(0, 100); }; + BENCHMARK("randomReal") { return randomReal(0.0, 1.0); }; + BENCHMARK("randomNorm") { return randomNormal(0.0, 10.0); }; +} diff --git a/test/benchmark/rcu.cpp b/test/benchmark/rcu.cpp new file mode 100644 index 00000000..4490fbb1 --- /dev/null +++ b/test/benchmark/rcu.cpp @@ -0,0 +1,24 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#include +#include +#include "utility/rcu.hpp" +#include +#include + +using namespace gapp::detail; +using namespace std::chrono_literals; + +std::shared_mutex rwlock; + +volatile size_t number = 0; +volatile std::atomic_size_t atomic_number = 0; +rcu_obj rcu_number = 0; + +TEST_CASE("rcu_lock", "[benchmark]") +{ + BENCHMARK("read") { return number; }; + BENCHMARK("atomic_fetch_add") { return atomic_number.fetch_add(1); }; + BENCHMARK("rwlock_read") { std::shared_lock _{ rwlock }; return number; }; + BENCHMARK("rcu_read") { std::scoped_lock _{ rcu_number }; return rcu_number.get(); }; +} diff --git a/test/benchmark/sample_unique.cpp b/test/benchmark/sample_unique.cpp index d61e65d2..80c13e9c 100644 --- a/test/benchmark/sample_unique.cpp +++ b/test/benchmark/sample_unique.cpp @@ -1,7 +1,6 @@ /* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ #include -#include #include #include "utility/rng.hpp" diff --git a/test/misc/CMakeLists.txt b/test/misc/CMakeLists.txt new file mode 100644 index 00000000..af9cdff9 --- /dev/null +++ b/test/misc/CMakeLists.txt @@ -0,0 +1,4 @@ + +add_executable(rcu_test "${CMAKE_CURRENT_SOURCE_DIR}/rcu.cpp") + +target_link_libraries(rcu_test PRIVATE gapp) diff --git a/test/misc/rcu.cpp b/test/misc/rcu.cpp new file mode 100644 index 00000000..3f98440f --- /dev/null +++ b/test/misc/rcu.cpp @@ -0,0 +1,53 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#include "utility/rcu.hpp" +#include +#include +#include +#include + +using namespace gapp::detail; +using namespace std::chrono_literals; + + +rcu_obj number = 0; + +static const auto reader_func = [] +{ + while (true) + { + std::scoped_lock _{ number }; + const size_t& n = number.get(); + std::this_thread::sleep_for(2ms); + assert(0 <= n && n <= 100); + } +}; + +static const auto writer_func = [i = 0]() mutable +{ + while (true) { number = i++ % 100; } +}; + +static const auto status_func = [] +{ + while (true) + { + std::cout << "Running RCU tests...\n"; + std::this_thread::sleep_for(5s); + } +}; + + +int main() +{ + std::jthread reader1{ reader_func }; + std::jthread reader2{ reader_func }; + std::jthread reader3{ reader_func }; + std::jthread reader4{ reader_func }; + std::jthread reader5{ reader_func }; + + std::jthread writer1{ writer_func }; + std::jthread writer2{ writer_func }; + + std::jthread status{ status_func }; +}