diff --git a/hist/histv7/inc/ROOT/RBinWithError.hxx b/hist/histv7/inc/ROOT/RBinWithError.hxx index bbaf491dc2050..ce665497b0d69 100644 --- a/hist/histv7/inc/ROOT/RBinWithError.hxx +++ b/hist/histv7/inc/ROOT/RBinWithError.hxx @@ -5,6 +5,8 @@ #ifndef ROOT_RBinWithError #define ROOT_RBinWithError +#include "RHistUtils.hxx" + namespace ROOT { namespace Experimental { @@ -45,6 +47,18 @@ struct RBinWithError final { fSum2 += rhs.fSum2; return *this; } + + void AtomicInc() + { + Internal::AtomicInc(&fSum); + Internal::AtomicInc(&fSum2); + } + + void AtomicAdd(double w) + { + Internal::AtomicAdd(&fSum, w); + Internal::AtomicAdd(&fSum2, w * w); + } }; } // namespace Experimental diff --git a/hist/histv7/inc/ROOT/RHistEngine.hxx b/hist/histv7/inc/ROOT/RHistEngine.hxx index f60485e061d65..61da9a4d9484e 100644 --- a/hist/histv7/inc/ROOT/RHistEngine.hxx +++ b/hist/histv7/inc/ROOT/RHistEngine.hxx @@ -326,6 +326,74 @@ public: } } + /// Fill an entry into the histogram using atomic instructions. + /// + /// \param[in] args the arguments for each axis + /// \see Fill(const std::tuple &args) + template + void FillAtomic(const std::tuple &args) + { + // We could rely on RAxes::ComputeGlobalIndex to check the number of arguments, but its exception message might + // be confusing for users. + if (sizeof...(A) != GetNDimensions()) { + throw std::invalid_argument("invalid number of arguments to Fill"); + } + RLinearizedIndex index = fAxes.ComputeGlobalIndexImpl(args); + if (index.fValid) { + assert(index.fIndex < fBinContents.size()); + Internal::AtomicInc(&fBinContents[index.fIndex]); + } + } + + /// Fill an entry into the histogram with a weight using atomic instructions. + /// + /// This overload is not available for integral bin content types (see \ref SupportsWeightedFilling). + /// + /// \param[in] args the arguments for each axis + /// \param[in] weight the weight for this entry + /// \see Fill(const std::tuple &args, RWeight weight) + template + void FillAtomic(const std::tuple &args, RWeight weight) + { + static_assert(SupportsWeightedFilling, "weighted filling is not supported for integral bin content types"); + + // We could rely on RAxes::ComputeGlobalIndex to check the number of arguments, but its exception message might + // be confusing for users. + if (sizeof...(A) != GetNDimensions()) { + throw std::invalid_argument("invalid number of arguments to Fill"); + } + RLinearizedIndex index = fAxes.ComputeGlobalIndexImpl(args); + if (index.fValid) { + assert(index.fIndex < fBinContents.size()); + Internal::AtomicAdd(&fBinContents[index.fIndex], weight.fValue); + } + } + + /// Fill an entry into the histogram using atomic instructions. + /// + /// \param[in] args the arguments for each axis + /// \see Fill(const A &...args) + template + void FillAtomic(const A &...args) + { + auto t = std::forward_as_tuple(args...); + if constexpr (std::is_same_v::type, RWeight>) { + static_assert(SupportsWeightedFilling, "weighted filling is not supported for integral bin content types"); + static constexpr std::size_t N = sizeof...(A) - 1; + if (N != fAxes.GetNDimensions()) { + throw std::invalid_argument("invalid number of arguments to Fill"); + } + RWeight weight = std::get(t); + RLinearizedIndex index = fAxes.ComputeGlobalIndexImpl(t); + if (index.fValid) { + assert(index.fIndex < fBinContents.size()); + Internal::AtomicAdd(&fBinContents[index.fIndex], weight.fValue); + } + } else { + FillAtomic(t); + } + } + /// %ROOT Streamer function to throw when trying to store an object of this class. void Streamer(TBuffer &) { throw std::runtime_error("unable to store RHistEngine"); } }; diff --git a/hist/histv7/inc/ROOT/RHistUtils.hxx b/hist/histv7/inc/ROOT/RHistUtils.hxx index e2e7788e80c98..8a8fccdc2cade 100644 --- a/hist/histv7/inc/ROOT/RHistUtils.hxx +++ b/hist/histv7/inc/ROOT/RHistUtils.hxx @@ -5,6 +5,12 @@ #ifndef ROOT_RHistUtils #define ROOT_RHistUtils +#include + +#ifdef _MSC_VER +#include +#endif + namespace ROOT { namespace Experimental { namespace Internal { @@ -16,6 +22,192 @@ struct LastType { using type = T; }; +#ifdef _MSC_VER +namespace MSVC { +template +struct AtomicOps {}; + +template <> +struct AtomicOps<1> { + static void Load(const void *ptr, void *ret) + { + *static_cast(ret) = __iso_volatile_load8(static_cast(ptr)); + } + static void Add(void *ptr, const void *val) + { + _InterlockedExchangeAdd8(static_cast(ptr), *static_cast(val)); + } + static bool CompareExchange(void *ptr, void *expected, const void *desired) + { + // MSVC functions have the arguments reversed... + const char expectedVal = *static_cast(expected); + const char desiredVal = *static_cast(desired); + const char previous = _InterlockedCompareExchange8(static_cast(ptr), desiredVal, expectedVal); + if (previous == expectedVal) { + return true; + } + *static_cast(expected) = previous; + return false; + } +}; + +template <> +struct AtomicOps<2> { + static void Load(const void *ptr, void *ret) + { + *static_cast(ret) = __iso_volatile_load16(static_cast(ptr)); + } + static void Add(void *ptr, const void *val) + { + _InterlockedExchangeAdd16(static_cast(ptr), *static_cast(val)); + } + static bool CompareExchange(void *ptr, void *expected, const void *desired) + { + // MSVC functions have the arguments reversed... + const short expectedVal = *static_cast(expected); + const short desiredVal = *static_cast(desired); + const short previous = _InterlockedCompareExchange16(static_cast(ptr), desiredVal, expectedVal); + if (previous == expectedVal) { + return true; + } + *static_cast(expected) = previous; + return false; + } +}; + +template <> +struct AtomicOps<4> { + static void Load(const void *ptr, void *ret) + { + *static_cast(ret) = __iso_volatile_load32(static_cast(ptr)); + } + static void Add(void *ptr, const void *val) + { + _InterlockedExchangeAdd(static_cast(ptr), *static_cast(val)); + } + static bool CompareExchange(void *ptr, void *expected, const void *desired) + { + // MSVC functions have the arguments reversed... + const long expectedVal = *static_cast(expected); + const long desiredVal = *static_cast(desired); + const long previous = _InterlockedCompareExchange(static_cast(ptr), desiredVal, expectedVal); + if (previous == expectedVal) { + return true; + } + *static_cast(expected) = previous; + return false; + } +}; + +template <> +struct AtomicOps<8> { + static void Load(const void *ptr, void *ret) + { + *static_cast<__int64 *>(ret) = __iso_volatile_load64(static_cast(ptr)); + } + static void Add(void *ptr, const void *val); + static bool CompareExchange(void *ptr, void *expected, const void *desired) + { + // MSVC functions have the arguments reversed... + const __int64 expectedVal = *static_cast<__int64 *>(expected); + const __int64 desiredVal = *static_cast(desired); + const __int64 previous = _InterlockedCompareExchange64(static_cast<__int64 *>(ptr), desiredVal, expectedVal); + if (previous == expectedVal) { + return true; + } + *static_cast<__int64 *>(expected) = previous; + return false; + } +}; +} // namespace MSVC +#endif + +template +void AtomicLoad(const T *ptr, T *ret) +{ +#ifndef _MSC_VER + __atomic_load(ptr, ret, __ATOMIC_RELAXED); +#else + MSVC::AtomicOps::Load(ptr, ret); +#endif +} + +template +bool AtomicCompareExchange(T *ptr, T *expected, T *desired) +{ +#ifndef _MSC_VER + return __atomic_compare_exchange(ptr, expected, desired, /*weak=*/false, __ATOMIC_RELAXED, __ATOMIC_RELAXED); +#else + return MSVC::AtomicOps::CompareExchange(ptr, expected, desired); +#endif +} + +template +void AtomicAddCompareExchangeLoop(T *ptr, T val) +{ + T expected; + AtomicLoad(ptr, &expected); + T desired = expected + val; + while (!AtomicCompareExchange(ptr, &expected, &desired)) { + // expected holds the new value; try again. + desired = expected + val; + } +} + +#ifdef _MSC_VER +namespace MSVC { +inline void AtomicOps<8>::Add(void *ptr, const void *val) +{ +#if _WIN64 + _InterlockedExchangeAdd64(static_cast<__int64 *>(ptr), *static_cast(val)); +#else + AtomicAddCompareExchangeLoop(static_cast<__int64 *>(ptr), *static_cast(val)); +#endif +} +} // namespace MSVC +#endif + +template +std::enable_if_t> AtomicAdd(T *ptr, T val) +{ +#ifndef _MSC_VER + __atomic_fetch_add(ptr, val, __ATOMIC_RELAXED); +#else + MSVC::AtomicOps::Add(ptr, &val); +#endif +} + +template +std::enable_if_t> AtomicAdd(T *ptr, T val) +{ + AtomicAddCompareExchangeLoop(ptr, val); +} + +// For adding a double-precision weight to a single-precision bin content type, cast the argument once before the +// compare-exchange loop. +static inline void AtomicAdd(float *ptr, double val) +{ + AtomicAdd(ptr, static_cast(val)); +} + +template +std::enable_if_t> AtomicInc(T *ptr) +{ + AtomicAdd(ptr, static_cast(1)); +} + +template +std::enable_if_t> AtomicAdd(T *ptr, const U &add) +{ + ptr->AtomicAdd(add); +} + +template +std::enable_if_t> AtomicInc(T *ptr) +{ + ptr->AtomicInc(); +} + } // namespace Internal } // namespace Experimental } // namespace ROOT diff --git a/hist/histv7/test/CMakeLists.txt b/hist/histv7/test/CMakeLists.txt index 36055aba99cc7..226ea3f976bcc 100644 --- a/hist/histv7/test/CMakeLists.txt +++ b/hist/histv7/test/CMakeLists.txt @@ -1,6 +1,8 @@ +HIST_ADD_GTEST(hist_atomic hist_atomic.cxx) HIST_ADD_GTEST(hist_axes hist_axes.cxx) HIST_ADD_GTEST(hist_categorical hist_categorical.cxx) HIST_ADD_GTEST(hist_engine hist_engine.cxx) +HIST_ADD_GTEST(hist_engine_atomic hist_engine_atomic.cxx) HIST_ADD_GTEST(hist_hist hist_hist.cxx) HIST_ADD_GTEST(hist_index hist_index.cxx) HIST_ADD_GTEST(hist_regular hist_regular.cxx) diff --git a/hist/histv7/test/hist_atomic.cxx b/hist/histv7/test/hist_atomic.cxx new file mode 100644 index 0000000000000..12f831b6cb54f --- /dev/null +++ b/hist/histv7/test/hist_atomic.cxx @@ -0,0 +1,55 @@ +#include "hist_test.hxx" + +#include + +#ifndef TYPED_TEST_SUITE +#define TYPED_TEST_SUITE TYPED_TEST_CASE +#endif + +template +class RHistAtomic : public testing::Test {}; + +using AtomicTypes = testing::Types; +TYPED_TEST_SUITE(RHistAtomic, AtomicTypes); + +TYPED_TEST(RHistAtomic, AtomicInc) +{ + TypeParam a = 1; + ROOT::Experimental::Internal::AtomicInc(&a); + EXPECT_EQ(a, 2); +} + +TYPED_TEST(RHistAtomic, AtomicAdd) +{ + TypeParam a = 1; + const TypeParam b = 2; + ROOT::Experimental::Internal::AtomicAdd(&a, b); + EXPECT_EQ(a, 3); +} + +// AtomicInc is implemented in terms of AtomicAdd, so it's sufficient to stress one of them. +TYPED_TEST(RHistAtomic, StressAtomicAdd) +{ + static constexpr TypeParam Addend = 1; + static constexpr std::size_t NThreads = 4; + // Reduce number of additions for char to avoid overflow. + static constexpr std::size_t NAddsPerThread = sizeof(TypeParam) == 1 ? 20 : 8000; + static constexpr std::size_t NAdds = NThreads * NAddsPerThread; + + TypeParam a = 0; + StressInParallel(NThreads, [&] { + for (std::size_t i = 0; i < NAddsPerThread; i++) { + ROOT::Experimental::Internal::AtomicAdd(&a, Addend); + } + }); + + EXPECT_EQ(a, NAdds * Addend); +} + +TEST(AtomicAdd, FloatDouble) +{ + float a = 1; + const double b = 2; + ROOT::Experimental::Internal::AtomicAdd(&a, b); + EXPECT_EQ(a, 3); +} diff --git a/hist/histv7/test/hist_engine_atomic.cxx b/hist/histv7/test/hist_engine_atomic.cxx new file mode 100644 index 0000000000000..af81163c328b9 --- /dev/null +++ b/hist/histv7/test/hist_engine_atomic.cxx @@ -0,0 +1,252 @@ +#include "hist_test.hxx" + +TEST(RHistEngine, FillAtomic) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.FillAtomic(-100); + for (std::size_t i = 0; i < Bins; i++) { + engine.FillAtomic(i); + } + engine.FillAtomic(100); + + EXPECT_EQ(engine.GetBinContent(RBinIndex::Underflow()), 1); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(engine.GetBinContent(index), 1); + } + EXPECT_EQ(engine.GetBinContent(RBinIndex::Overflow()), 1); + + // Instantiate further bin content types to make sure they work. + RHistEngine engineL({axis}); + engineL.FillAtomic(1); + + RHistEngine engineLL({axis}); + engineLL.FillAtomic(1); + + RHistEngine engineF({axis}); + engineF.FillAtomic(1); + + RHistEngine engineD({axis}); + engineD.FillAtomic(1); +} + +TEST(RHistEngine, StressFillAtomic) +{ + static constexpr std::size_t NThreads = 4; + static constexpr std::size_t NFillsPerThread = 10000; + static constexpr std::size_t NFills = NThreads * NFillsPerThread; + + // Fill a single bin, to maximize contention. + RHistEngine engine(1, {0, 1}); + StressInParallel(NThreads, [&] { + for (std::size_t i = 0; i < NFillsPerThread; i++) { + engine.FillAtomic(0.5); + } + }); + + EXPECT_EQ(engine.GetBinContent(0), NFills); +} + +TEST(RHistEngine, FillAtomicTuple) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.FillAtomic(std::make_tuple(-100)); + for (std::size_t i = 0; i < Bins; i++) { + engine.FillAtomic(std::make_tuple(i)); + } + engine.FillAtomic(std::make_tuple(100)); + + EXPECT_EQ(engine.GetBinContent(RBinIndex::Underflow()), 1); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(engine.GetBinContent(index), 1); + } + EXPECT_EQ(engine.GetBinContent(RBinIndex::Overflow()), 1); +} + +TEST(RHistEngine, FillAtomicInvalidNumberOfArguments) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine1({axis}); + ASSERT_EQ(engine1.GetNDimensions(), 1); + RHistEngine engine2({axis, axis}); + ASSERT_EQ(engine2.GetNDimensions(), 2); + + EXPECT_NO_THROW(engine1.FillAtomic(1)); + EXPECT_THROW(engine1.FillAtomic(1, 2), std::invalid_argument); + + EXPECT_THROW(engine2.FillAtomic(1), std::invalid_argument); + EXPECT_NO_THROW(engine2.FillAtomic(1, 2)); + EXPECT_THROW(engine2.FillAtomic(1, 2, 3), std::invalid_argument); +} + +TEST(RHistEngine, FillAtomicWeight) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.FillAtomic(-100, RWeight(0.25)); + for (std::size_t i = 0; i < Bins; i++) { + engine.FillAtomic(i, RWeight(0.1 + i * 0.03)); + } + engine.FillAtomic(100, RWeight(0.75)); + + EXPECT_FLOAT_EQ(engine.GetBinContent(RBinIndex::Underflow()), 0.25); + for (auto index : axis.GetNormalRange()) { + EXPECT_FLOAT_EQ(engine.GetBinContent(index), 0.1 + index.GetIndex() * 0.03); + } + EXPECT_EQ(engine.GetBinContent(RBinIndex::Overflow()), 0.75); + + // Instantiate further bin content types to make sure they work. + RHistEngine engineD({axis}); + engineD.FillAtomic(1, RWeight(0.8)); +} + +TEST(RHistEngine, StressFillAtomicWeight) +{ + static constexpr std::size_t NThreads = 4; + static constexpr std::size_t NFillsPerThread = 10000; + static constexpr std::size_t NFills = NThreads * NFillsPerThread; + static constexpr double Weight = 0.5; + + // Fill a single bin, to maximize contention. + RHistEngine engine(1, {0, 1}); + StressInParallel(NThreads, [&] { + for (std::size_t i = 0; i < NFillsPerThread; i++) { + engine.FillAtomic(0.5, RWeight(Weight)); + } + }); + + EXPECT_EQ(engine.GetBinContent(0), NFills * Weight); +} + +TEST(RHistEngine, FillAtomicTupleWeight) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.FillAtomic(std::make_tuple(-100), RWeight(0.25)); + for (std::size_t i = 0; i < Bins; i++) { + engine.FillAtomic(std::make_tuple(i), RWeight(0.1 + i * 0.03)); + } + engine.FillAtomic(std::make_tuple(100), RWeight(0.75)); + + EXPECT_FLOAT_EQ(engine.GetBinContent(RBinIndex::Underflow()), 0.25); + for (auto index : axis.GetNormalRange()) { + EXPECT_FLOAT_EQ(engine.GetBinContent(index), 0.1 + index.GetIndex() * 0.03); + } + EXPECT_EQ(engine.GetBinContent(RBinIndex::Overflow()), 0.75); +} + +TEST(RHistEngine, FillAtomicWeightInvalidNumberOfArguments) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine1({axis}); + ASSERT_EQ(engine1.GetNDimensions(), 1); + RHistEngine engine2({axis, axis}); + ASSERT_EQ(engine2.GetNDimensions(), 2); + + EXPECT_NO_THROW(engine1.FillAtomic(1, RWeight(1))); + EXPECT_THROW(engine1.FillAtomic(1, 2, RWeight(1)), std::invalid_argument); + + EXPECT_THROW(engine2.FillAtomic(1, RWeight(1)), std::invalid_argument); + EXPECT_NO_THROW(engine2.FillAtomic(1, 2, RWeight(1))); + EXPECT_THROW(engine2.FillAtomic(1, 2, 3, RWeight(1)), std::invalid_argument); +} + +TEST(RHistEngine, FillAtomicTupleWeightInvalidNumberOfArguments) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine1({axis}); + ASSERT_EQ(engine1.GetNDimensions(), 1); + RHistEngine engine2({axis, axis}); + ASSERT_EQ(engine2.GetNDimensions(), 2); + + EXPECT_NO_THROW(engine1.FillAtomic(std::make_tuple(1), RWeight(1))); + EXPECT_THROW(engine1.FillAtomic(std::make_tuple(1, 2), RWeight(1)), std::invalid_argument); + + EXPECT_THROW(engine2.FillAtomic(std::make_tuple(1), RWeight(1)), std::invalid_argument); + EXPECT_NO_THROW(engine2.FillAtomic(std::make_tuple(1, 2), RWeight(1))); + EXPECT_THROW(engine2.FillAtomic(std::make_tuple(1, 2, 3), RWeight(1)), std::invalid_argument); +} + +TEST(RHistEngine_RBinWithError, FillAtomic) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + for (std::size_t i = 0; i < Bins; i++) { + engine.FillAtomic(i); + } + + for (auto index : axis.GetNormalRange()) { + auto &bin = engine.GetBinContent(index); + EXPECT_EQ(bin.fSum, 1); + EXPECT_EQ(bin.fSum2, 1); + } +} + +TEST(RHistEngine_RBinWithError, StressFillAtomic) +{ + static constexpr std::size_t NThreads = 4; + static constexpr std::size_t NFillsPerThread = 10000; + static constexpr std::size_t NFills = NThreads * NFillsPerThread; + + // Fill a single bin, to maximize contention. + RHistEngine engine(1, {0, 1}); + StressInParallel(NThreads, [&] { + for (std::size_t i = 0; i < NFillsPerThread; i++) { + engine.FillAtomic(0.5); + } + }); + + EXPECT_EQ(engine.GetBinContent(0).fSum, NFills); + EXPECT_EQ(engine.GetBinContent(0).fSum2, NFills); +} + +TEST(RHistEngine_RBinWithError, FillAtomicWeight) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + for (std::size_t i = 0; i < Bins; i++) { + engine.FillAtomic(i, RWeight(0.1 + i * 0.03)); + } + + for (auto index : axis.GetNormalRange()) { + auto &bin = engine.GetBinContent(index); + double weight = 0.1 + index.GetIndex() * 0.03; + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } +} + +TEST(RHistEngine_RBinWithError, StressFillAtomicWeight) +{ + static constexpr std::size_t NThreads = 4; + static constexpr std::size_t NFillsPerThread = 10000; + static constexpr std::size_t NFills = NThreads * NFillsPerThread; + static constexpr double Weight = 0.5; + + // Fill a single bin, to maximize contention. + RHistEngine engine(1, {0, 1}); + StressInParallel(NThreads, [&] { + for (std::size_t i = 0; i < NFillsPerThread; i++) { + engine.FillAtomic(0.5, RWeight(Weight)); + } + }); + + EXPECT_EQ(engine.GetBinContent(0).fSum, NFills * Weight); + EXPECT_EQ(engine.GetBinContent(0).fSum2, NFills * Weight * Weight); +} diff --git a/hist/histv7/test/hist_test.hxx b/hist/histv7/test/hist_test.hxx index b9109388836f7..33a3ae64fe4da 100644 --- a/hist/histv7/test/hist_test.hxx +++ b/hist/histv7/test/hist_test.hxx @@ -13,8 +13,6 @@ #include #include -#include "gtest/gtest.h" - using ROOT::Experimental::RAxisVariant; using ROOT::Experimental::RBinIndex; using ROOT::Experimental::RBinIndexRange; @@ -28,4 +26,32 @@ using ROOT::Experimental::RVariableBinAxis; using ROOT::Experimental::RWeight; using ROOT::Experimental::Internal::RAxes; +#include + +#include +#include +#include +#include + +template +void StressInParallel(std::size_t nThreads, Work &&w) +{ + std::atomic flag; + + std::vector threads; + for (std::size_t i = 0; i < nThreads; i++) { + threads.emplace_back([&] { + while (!flag) { + // Wait for all threads to be started. + } + w(); + }); + } + + flag = true; + for (std::size_t i = 0; i < nThreads; i++) { + threads[i].join(); + } +} + #endif diff --git a/hist/histv7/test/hist_user.cxx b/hist/histv7/test/hist_user.cxx index cb893b46520ab..3d7a37119be07 100644 --- a/hist/histv7/test/hist_user.cxx +++ b/hist/histv7/test/hist_user.cxx @@ -30,6 +30,10 @@ struct User { fValue += rhs.fValue; return *this; } + + void AtomicInc() { ROOT::Experimental::Internal::AtomicInc(&fValue); } + + void AtomicAdd(double w) { ROOT::Experimental::Internal::AtomicAdd(&fValue, w); } }; static_assert(std::is_nothrow_move_constructible_v>); @@ -119,3 +123,33 @@ TEST(RHistEngineUser, FillWeight) std::array indices = {9}; EXPECT_EQ(engine.GetBinContent(indices).fValue, 0.9); } + +TEST(RHistEngineUser, FillAtomic) +{ + // Unweighted filling with atomic instructions uses AtomicInc + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.FillAtomic(8.5); + engine.FillAtomic(std::make_tuple(9.5)); + + EXPECT_EQ(engine.GetBinContent(RBinIndex(8)).fValue, 1); + std::array indices = {9}; + EXPECT_EQ(engine.GetBinContent(indices).fValue, 1); +} + +TEST(RHistEngineUser, FillAtomicWeight) +{ + // Weighted filling with atomic instructions uses AtomicAdd + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.FillAtomic(8.5, RWeight(0.8)); + engine.FillAtomic(std::make_tuple(9.5), RWeight(0.9)); + + EXPECT_EQ(engine.GetBinContent(RBinIndex(8)).fValue, 0.8); + std::array indices = {9}; + EXPECT_EQ(engine.GetBinContent(indices).fValue, 0.9); +}