Skip to content

Commit 8ee13d7

Browse files
authored
Merge pull request ceph#65716 from ronen-fr/wip-rf-just-mode
common: ModeCollector: locating the value of the mode Reviewed-by: Alex Ainscow <[email protected]>
2 parents cf11bef + 3efcdbf commit 8ee13d7

File tree

3 files changed

+642
-0
lines changed

3 files changed

+642
-0
lines changed

src/common/mode_collector.h

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2+
// vim: ts=8 sw=2 smarttab
3+
#pragma once
4+
/**
5+
* \file A (small) container for fast mode lookups
6+
* ('mode' here is the statistical mode of a set of values, i.e. the
7+
* value that appears most frequently in the set).
8+
*/
9+
10+
#include <fmt/format.h>
11+
12+
#include <algorithm>
13+
#include <array>
14+
#include <cassert>
15+
#include <cstddef>
16+
#include <functional>
17+
#include <memory_resource>
18+
#include <ranges>
19+
#include <unordered_map>
20+
21+
/**
22+
* ModeCollector is designed to collect a set of values (e.g. - the data digest
23+
* reported by each replica), associating each value with an object ID (in our
24+
* example - the replica ID), and efficiently finding the mode (the value that
25+
* appears most frequently) of the collected values.
26+
*
27+
* The template parameters are:
28+
* - OBJ_ID: The type of the object ID (e.g., replica ID).
29+
* - K: The type of the value being collected.
30+
* - HSH: The hash function for K, to be used with the unordered_map.
31+
* Note: if HSH is std::identity, then K must fit in size_t.
32+
* - MAX_ELEM is used to calculate the estimated memory footprint of the
33+
* unordered_map.
34+
*
35+
* ModeCollector uses a monotonic buffer resource to manage memory
36+
* efficiently, avoiding frequent allocations and deallocations.
37+
* My tests (see link for details and caveats) show that using the PMR
38+
* allocator speeds up the mode-finding process by 20% to 40%.
39+
*/
40+
41+
struct ModeFinder {
42+
43+
/// a 'non-templated' version of mode_status_t, to simplify usage.
44+
enum class mode_status_t {
45+
no_mode_value, ///< No clear victory for any value
46+
mode_value, ///< we have a winner, but it appears in less than half
47+
///< of the samples
48+
authorative_value ///< more than half of the samples are of the same value
49+
};
50+
};
51+
52+
// note the use of std::identity: it's a pretty fast hash function,
53+
// but we are restricted to size_t sized keys (per stdlib implementation
54+
// of the unrdered map).
55+
56+
template <
57+
typename OBJ_ID, ///< how to identify the object that reported a value
58+
typename K, ///< the type of the value being collected
59+
typename HSH = std::identity, ///< the hash function for K
60+
int MAX_ELEM = 12>
61+
requires(
62+
std::invocable<HSH, K> &&
63+
sizeof(std::invoke_result_t<HSH, K>) <= sizeof(size_t))
64+
class ModeCollector : public ModeFinder {
65+
private:
66+
struct node_type_t {
67+
size_t m_count{0};
68+
OBJ_ID m_id; ///< Stores the object ID associated with this value
69+
};
70+
71+
// estimated (upper limit) memory footprint of the unordered_map
72+
// vvvvvvvvvvvvvvvvvvvvvvvvvvvv
73+
// Bucket array: typically 2x num_elements for good load factor
74+
static const size_t bucket_array_size = (MAX_ELEM * 2) * sizeof(void*);
75+
// Node storage: each elem needs hash + next-ptr
76+
static constexpr size_t node_overhead = sizeof(void*) + sizeof(size_t);
77+
static constexpr size_t node_storage =
78+
MAX_ELEM * (sizeof(K) + sizeof(node_type_t) + node_overhead);
79+
// PMR allocator overhead (alignment, bookkeeping)
80+
static constexpr size_t pmr_overhead_per_alloc = 16; // typical
81+
// bucket array + nodes
82+
static constexpr size_t total_overhead = pmr_overhead_per_alloc * 2;
83+
static constexpr size_t m_estimated_memory_footprint =
84+
bucket_array_size + node_storage + total_overhead;
85+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
86+
87+
std::array<std::byte, m_estimated_memory_footprint> m_buffer;
88+
std::pmr::monotonic_buffer_resource m_mbr{m_buffer.data(), m_buffer.size()};
89+
90+
/// Map to store the occurrence count of each value
91+
std::pmr::unordered_map<
92+
K,
93+
node_type_t,
94+
HSH,
95+
std::equal_to<K> >
96+
m_frequency_map;
97+
98+
/// Actual count of elements added
99+
size_t m_actual_count{0};
100+
101+
public:
102+
using mode_status_t = ModeFinder::mode_status_t;
103+
104+
struct results_t {
105+
/// do we have a mode value?
106+
mode_status_t tag;
107+
/// the mode value (if any)
108+
K key;
109+
/// an object ID, "arbitrary" selected from the set of objects that
110+
/// reported the mode value
111+
OBJ_ID id;
112+
/// the number of times the mode value was reported
113+
size_t count;
114+
auto operator<=>(const results_t& rhs) const = default;
115+
};
116+
117+
explicit ModeCollector() : m_frequency_map(&m_mbr)
118+
{
119+
m_frequency_map.reserve(MAX_ELEM);
120+
}
121+
122+
/// Add a value to the collector
123+
void insert(const OBJ_ID& obj, const K& value) noexcept
124+
{
125+
auto& node = m_frequency_map[value];
126+
node.m_count++;
127+
// Store the object ID associated with this value
128+
// (note: it's OK to overwrite the ID here)
129+
node.m_id = obj;
130+
m_actual_count++;
131+
}
132+
133+
134+
/**
135+
* Find the mode of the collected values
136+
*
137+
* Note: we are losing ~4% performance due to find_mode() not being noexcept.
138+
*/
139+
results_t find_mode()
140+
{
141+
assert(!m_frequency_map.empty());
142+
143+
auto max_elem = std::ranges::max_element(
144+
m_frequency_map, {},
145+
[](const auto& pair) { return pair.second.m_count; });
146+
147+
// Check for clear victory
148+
if (max_elem->second.m_count > m_actual_count / 2) {
149+
return {
150+
mode_status_t::authorative_value, max_elem->first,
151+
max_elem->second.m_id, max_elem->second.m_count};
152+
}
153+
154+
// Check for possible ties
155+
const auto max_elem_cnt = max_elem->second.m_count;
156+
157+
max_elem->second.m_count = 0; // Reset the count of the max element
158+
const auto second_best_elem = std::ranges::max_element(
159+
m_frequency_map, {},
160+
[](const auto& pair) { return pair.second.m_count; });
161+
max_elem->second.m_count = max_elem_cnt; // Restore the count
162+
163+
if (second_best_elem->second.m_count == max_elem_cnt) {
164+
return {
165+
mode_status_t::no_mode_value, max_elem->first, max_elem->second.m_id,
166+
max_elem_cnt};
167+
}
168+
169+
return {
170+
mode_status_t::mode_value, max_elem->first, max_elem->second.m_id,
171+
max_elem_cnt};
172+
}
173+
};
174+

src/test/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,12 @@ add_executable(unittest_not_before_queue
10131013
add_ceph_unittest(unittest_not_before_queue)
10141014
target_link_libraries(unittest_not_before_queue ceph-common)
10151015

1016+
# unittest_mode_collector
1017+
add_executable(unittest_mode_collector
1018+
test_mode_collector.cc)
1019+
add_ceph_unittest(unittest_mode_collector)
1020+
target_link_libraries(unittest_mode_collector ceph-common)
1021+
10161022
if(NOT WIN32)
10171023
# unittest_on_exit
10181024
add_executable(unittest_on_exit

0 commit comments

Comments
 (0)