Skip to content

Commit 863501e

Browse files
committed
[hist] Pause filling during snapshot
FillAtomic is much faster than SnapshotAtomic, so in case of heavy contention it would starve the snapshot. By adding a new atomic flag FillAtomic can pause while a Snapshot is running. The successful double collect is still needed if a thread is already in FillAtomic. If SnapshotAtomic is not used, the performance impact of the added check is minimal / not measurable because fSnapshotInProgress is always false and already in the cache line of the RHistEngine.
1 parent 3990b5e commit 863501e

3 files changed

Lines changed: 67 additions & 3 deletions

File tree

hist/histv7/inc/ROOT/RHistEngine.hxx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class RHistEngine final {
8484
/// The bin contents for this histogram
8585
std::vector<BinContentType> fBinContents;
8686

87+
/// Flag to pause filling while a snapshot is ongoing
88+
mutable std::atomic<bool> fSnapshotInProgress{false}; //!
89+
8790
public:
8891
/// Construct a histogram engine.
8992
///
@@ -134,7 +137,7 @@ public:
134137
/// Efficiently move construct a histogram engine.
135138
///
136139
/// After this operation, the moved-from object is invalid.
137-
RHistEngine(RHistEngine &&) = default;
140+
RHistEngine(RHistEngine &&rhs) noexcept : fAxes(std::move(rhs.fAxes)), fBinContents(std::move(rhs.fBinContents)) {}
138141

139142
/// The copy assignment operator is deleted.
140143
///
@@ -144,7 +147,12 @@ public:
144147
/// Efficiently move a histogram engine.
145148
///
146149
/// After this operation, the moved-from object is invalid.
147-
RHistEngine &operator=(RHistEngine &&) = default;
150+
RHistEngine &operator=(RHistEngine &&rhs) noexcept
151+
{
152+
std::swap(fAxes, rhs.fAxes);
153+
std::swap(fBinContents, rhs.fBinContents);
154+
return *this;
155+
}
148156

149157
~RHistEngine() = default;
150158

@@ -513,6 +521,10 @@ public:
513521
template <typename... A>
514522
void FillAtomic(const std::tuple<A...> &args)
515523
{
524+
while (fSnapshotInProgress.load(std::memory_order_relaxed)) {
525+
// Spin while a snapshot is running
526+
}
527+
516528
// We could rely on RAxes::ComputeGlobalIndex to check the number of arguments, but its exception message might
517529
// be confusing for users.
518530
if (sizeof...(A) != GetNDimensions()) {
@@ -537,6 +549,10 @@ public:
537549
{
538550
static_assert(SupportsWeightedFilling, "weighted filling is not supported for integral bin content types");
539551

552+
while (fSnapshotInProgress.load(std::memory_order_relaxed)) {
553+
// Spin while a snapshot is running
554+
}
555+
540556
// We could rely on RAxes::ComputeGlobalIndex to check the number of arguments, but its exception message might
541557
// be confusing for users.
542558
if (sizeof...(A) != GetNDimensions()) {
@@ -562,6 +578,10 @@ public:
562578
static_assert(std::is_class_v<BinContentType>,
563579
"user-defined weight types are only supported for user-defined bin content types");
564580

581+
while (fSnapshotInProgress.load(std::memory_order_relaxed)) {
582+
// Spin while a snapshot is running
583+
}
584+
565585
// We could rely on RAxes::ComputeGlobalIndex to check the number of arguments, but its exception message might
566586
// be confusing for users.
567587
if (sizeof...(A) != GetNDimensions()) {
@@ -583,6 +603,10 @@ public:
583603
{
584604
static_assert(sizeof...(A) >= 1, "need at least one argument to Fill");
585605
if constexpr (sizeof...(A) >= 1) {
606+
while (fSnapshotInProgress.load(std::memory_order_relaxed)) {
607+
// Spin while a snapshot is running
608+
}
609+
586610
auto t = std::forward_as_tuple(args...);
587611
if constexpr (std::is_same_v<typename Internal::LastType<A...>::type, RWeight>) {
588612
static_assert(SupportsWeightedFilling, "weighted filling is not supported for integral bin content types");
@@ -846,6 +870,12 @@ public:
846870
static_assert(std::is_trivially_copyable_v<BinContentType>,
847871
"snapshotting requires a trivially copyable bin content type");
848872

873+
do {
874+
while (fSnapshotInProgress.load(std::memory_order_relaxed)) {
875+
// Spin while another snapshot is running
876+
}
877+
} while (fSnapshotInProgress.exchange(true, std::memory_order_relaxed));
878+
849879
RHistEngine snapshot(fAxes.Get());
850880
// Do a first collect.
851881
for (std::size_t i = 0; i < fBinContents.size(); i++) {
@@ -871,6 +901,8 @@ public:
871901
}
872902
} while (changed);
873903

904+
fSnapshotInProgress.store(false, std::memory_order_relaxed);
905+
874906
return snapshot;
875907
}
876908

hist/histv7/test/hist_engine_atomic.cxx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,37 @@ TEST(RHistEngine, SnapshotAtomic)
310310
EXPECT_EQ(engineB.GetBinContent(RBinIndex::Overflow()), 1);
311311
}
312312

313+
// Stress calling SnapshotAtomic from multiple threads.
313314
TEST(RHistEngine, StressSnapshotAtomic)
315+
{
316+
static constexpr std::size_t Bins = 20;
317+
static constexpr std::size_t NThreads = 4;
318+
static constexpr std::size_t NSnapshotsPerThread = 10000;
319+
static constexpr int ExpectedBinContent0 = 1;
320+
321+
// Create a histogram with some bins that takes a bit of time to snapshot.
322+
RHistEngine<int> engine(Bins, {0, Bins});
323+
engine.Fill(0.5);
324+
325+
std::atomic<int> binContent0 = ExpectedBinContent0;
326+
327+
StressInParallel(NThreads, [&] {
328+
for (std::size_t i = 0; i < NSnapshotsPerThread; i++) {
329+
auto snapshot = engine.SnapshotAtomic();
330+
// compare_exchange wants a non-const reference...
331+
int expected = ExpectedBinContent0;
332+
int actual = snapshot.GetBinContent(0);
333+
if (actual != expected) {
334+
binContent0.compare_exchange_strong(expected, actual);
335+
}
336+
}
337+
});
338+
339+
EXPECT_EQ(binContent0, ExpectedBinContent0);
340+
}
341+
342+
// Stress calling SnapshotAtomic while other threads call FillAtomic.
343+
TEST(RHistEngine, StressFillSnapshotAtomic)
314344
{
315345
static constexpr std::size_t Bins = 20;
316346
static constexpr std::size_t NThreads = 4;
@@ -485,7 +515,7 @@ TEST(RHistEngine_RBinWithError, SnapshotAtomic)
485515
EXPECT_EQ(engineB.GetBinContent(RBinIndex::Overflow()).fSum, 1);
486516
}
487517

488-
TEST(RHistEngine_RBinWithError, StressSnapshotAtomic)
518+
TEST(RHistEngine_RBinWithError, StressFillSnapshotAtomic)
489519
{
490520
static constexpr std::size_t Bins = 20;
491521
static constexpr std::size_t NThreads = 4;

hist/histv7/test/hist_io.cxx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ static void ExpectThrowOnWriteObject(const T &obj)
1010
{
1111
ROOT::TestSupport::CheckDiagsRAII diagRAII;
1212
diagRAII.optionalDiag(kWarning, "TKey::TKey", "no public constructor", /*matchFullMessage=*/false);
13+
diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "data member \"fSnapshotInProgress\" will not be saved",
14+
/*matchFullMessage=*/false);
1315

1416
TMemFile f("mem.root", "RECREATE");
1517
EXPECT_THROW(f.WriteObject(&obj, "o"), std::runtime_error);

0 commit comments

Comments
 (0)