Skip to content

Commit 3efcdbf

Browse files
committed
common: ModeCollector: locating the value of the mode
The ModeCollector class is used to collect values of some type 'key', each associated with some object identified by an 'ID'. The collector reports the 'mode' value - the value associated with the largest number of distinct IDs. The results structure returned by the collector specifies one of three possible mode_status_t values: - no_mode_value - No clear victory for any value - mode_value - we have a winner, but it has less than half of the samples - authorative_value - more than half of the samples are of the same value Signed-off-by: Ronen Friedman <[email protected]>
1 parent afa88a4 commit 3efcdbf

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)