Skip to content

Commit d93478a

Browse files
committed
Transactional hash map hash_map_tm test
1 parent 68627a0 commit d93478a

File tree

6 files changed

+328
-4
lines changed

6 files changed

+328
-4
lines changed

internals/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
add_conventional_library(testing PUBLIC trade_v1)
1+
add_conventional_library(testing)
2+
target_link_libraries(testing INTERFACE dumpster_v1 trade_v1)
3+
24
add_conventional_executable_tests(PRIVATE testing trade_v1 testing_v1 std_thread)

internals/include/testing/config.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#pragma once
22

3+
#include "dumpster_v1/config.hpp"
34
#include "trade_v1/config.hpp"
45

56
namespace testing {
67

8+
namespace dumpster = dumpster_v1;
79
namespace trade = trade_v1;
810

9-
}
11+
} // namespace testing
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#pragma once
2+
3+
#include "testing/config.hpp"
4+
5+
#include "trade_v1/trade.hpp"
6+
7+
#include "dumpster_v1/primes.hpp"
8+
9+
#include "polyfill_v1/memory.hpp"
10+
#include <functional>
11+
#include <optional>
12+
#include <utility>
13+
14+
namespace testing {
15+
16+
/// A transactional hash map for testing purposes.
17+
template <class Key,
18+
class Mapped,
19+
class Hash = std::hash<Key>,
20+
class Equal = std::equal_to<Key>>
21+
class hash_map_tm;
22+
23+
class hash_map_tm_private {
24+
template <class, class, class, class> friend class hash_map_tm;
25+
26+
// This hack is a workaround for not having std::shared_ptr<T[]> support
27+
// in AppleClang.
28+
template <class T> struct array_hack {
29+
void operator delete(void *self) { delete[] reinterpret_cast<T *>(self); }
30+
T &at(size_t i) { return reinterpret_cast<T *>(this)[i]; }
31+
};
32+
};
33+
34+
template <class Key, class Mapped, class Hash, class Equal>
35+
class hash_map_tm : hash_map_tm_private {
36+
template <class T> using ptr_t = std::shared_ptr<T>;
37+
38+
struct node_t;
39+
40+
using link = trade::atom<std::shared_ptr<node_t>>;
41+
42+
trade::atom<size_t> m_item_count;
43+
trade::atom<size_t> m_buckets_count;
44+
trade::atom<ptr_t<array_hack<link>>> m_buckets;
45+
46+
public:
47+
using size_type = size_t;
48+
49+
using key_type = Key;
50+
using mapped_type = Mapped;
51+
52+
hash_map_tm();
53+
54+
hash_map_tm(const hash_map_tm &) = delete;
55+
hash_map_tm &operator=(const hash_map_tm &) = delete;
56+
57+
size_t size() const;
58+
59+
bool empty() const;
60+
61+
void clear();
62+
63+
void swap(hash_map_tm &that);
64+
65+
template <class ForwardableMapped, class Config = trade::stack_t<1024>>
66+
bool add_or_set(const Key &key,
67+
ForwardableMapped &&mapped,
68+
Config config = trade::stack<1024>);
69+
70+
std::optional<Mapped> try_get(const Key &key) const;
71+
72+
bool remove(const Key &key);
73+
74+
#ifndef NDEBUG
75+
static std::atomic<size_t> s_live_nodes; // Only for testing purposes
76+
#endif
77+
};
78+
79+
// -----------------------------------------------------------------------------
80+
81+
template <class Key, class Mapped, class Hash, class Equal>
82+
struct hash_map_tm<Key, Mapped, Hash, Equal>::node_t {
83+
#ifndef NDEBUG
84+
~node_t() { --s_live_nodes; }
85+
#endif
86+
template <class ForwardableKey, class ForwardableMapped>
87+
node_t(ForwardableKey &&key, ForwardableMapped &&value)
88+
: m_next(nullptr), m_key(std::forward<ForwardableKey>(key)),
89+
m_mapped(std::forward<ForwardableMapped>(value)) {
90+
#ifndef NDEBUG
91+
++s_live_nodes;
92+
#endif
93+
}
94+
link m_next;
95+
const Key m_key;
96+
trade::atom<Mapped> m_mapped;
97+
};
98+
99+
//
100+
101+
template <class Key, class Mapped, class Hash, class Equal>
102+
hash_map_tm<Key, Mapped, Hash, Equal>::hash_map_tm()
103+
: m_item_count(0), m_buckets_count(0), m_buckets(nullptr) {}
104+
105+
template <class Key, class Mapped, class Hash, class Equal>
106+
size_t hash_map_tm<Key, Mapped, Hash, Equal>::size() const {
107+
return trade::atomically(trade::assume_readonly,
108+
[&]() { return m_item_count.load(); });
109+
}
110+
111+
template <class Key, class Mapped, class Hash, class Equal>
112+
bool hash_map_tm<Key, Mapped, Hash, Equal>::empty() const {
113+
return trade::atomically(trade::assume_readonly,
114+
[&]() { return m_item_count == 0; });
115+
}
116+
117+
template <class Key, class Mapped, class Hash, class Equal>
118+
void hash_map_tm<Key, Mapped, Hash, Equal>::clear() {
119+
trade::atomically([&]() {
120+
m_item_count = 0;
121+
m_buckets_count = 0;
122+
m_buckets = nullptr;
123+
});
124+
}
125+
126+
template <class Key, class Mapped, class Hash, class Equal>
127+
void hash_map_tm<Key, Mapped, Hash, Equal>::swap(hash_map_tm &that) {
128+
trade::atomically([&]() {
129+
std::swap(m_item_count.ref(), that.m_item_count.ref());
130+
std::swap(m_buckets_count.ref(), that.m_buckets_count.ref());
131+
std::swap(m_buckets.ref(), that.m_buckets.ref());
132+
});
133+
}
134+
135+
template <class Key, class Mapped, class Hash, class Equal>
136+
template <class ForwardableMapped, class Config>
137+
bool hash_map_tm<Key, Mapped, Hash, Equal>::add_or_set(
138+
const Key &key, ForwardableMapped &&mapped, Config config) {
139+
auto key_hash = Hash()(key);
140+
141+
return trade::atomically(config, [&]() {
142+
auto item_count = m_item_count.load();
143+
auto buckets_count = m_buckets_count.load();
144+
auto buckets = m_buckets.load();
145+
146+
if (buckets_count <= item_count) {
147+
auto old_buckets = std::move(buckets);
148+
auto old_buckets_count = buckets_count;
149+
150+
m_buckets_count = buckets_count =
151+
dumpster::prime_less_than_next_pow_2_or_1(old_buckets_count * 2 + 1);
152+
m_buckets = buckets = ptr_t<array_hack<link>>(
153+
reinterpret_cast<array_hack<link> *>(new link[buckets_count]));
154+
155+
for (size_t i = 0; i < old_buckets_count; ++i) {
156+
auto work = old_buckets->at(i).load();
157+
while (work) {
158+
auto &ref_next = work->m_next.ref();
159+
auto &ref_bucket =
160+
buckets->at(Hash()(work->m_key) % buckets_count).ref();
161+
auto next = std::move(ref_next);
162+
ref_next = std::move(ref_bucket);
163+
ref_bucket = std::move(work);
164+
work = std::move(next);
165+
}
166+
}
167+
}
168+
169+
auto prev = &buckets->at(key_hash % buckets_count);
170+
while (true) {
171+
if (auto node = prev->load()) {
172+
if (Equal()(node->m_key, key)) {
173+
node->m_mapped = std::forward<ForwardableMapped>(mapped);
174+
return false;
175+
} else {
176+
prev = &node->m_next;
177+
}
178+
} else {
179+
prev->ref().reset(
180+
new node_t(key, std::forward<ForwardableMapped>(mapped)));
181+
m_item_count = item_count + 1;
182+
return true;
183+
}
184+
}
185+
});
186+
}
187+
188+
template <class Key, class Mapped, class Hash, class Equal>
189+
std::optional<Mapped>
190+
hash_map_tm<Key, Mapped, Hash, Equal>::try_get(const Key &key) const {
191+
auto key_hash = Hash()(key);
192+
return trade::atomically(
193+
trade::assume_readonly, [&]() -> std::optional<Mapped> {
194+
if (auto buckets_count = m_buckets_count.load())
195+
for (auto node =
196+
m_buckets.load()->at(key_hash % buckets_count).load();
197+
node;
198+
node = node->m_next)
199+
if (Equal()(node->m_key, key))
200+
return node->m_mapped.load();
201+
return std::nullopt;
202+
});
203+
}
204+
205+
template <class Key, class Mapped, class Hash, class Equal>
206+
bool hash_map_tm<Key, Mapped, Hash, Equal>::remove(const Key &key) {
207+
auto key_hash = Hash()(key);
208+
return trade::atomically([&]() {
209+
if (auto buckets_count = m_buckets_count.load()) {
210+
auto prev = &m_buckets.load()->at(key_hash % buckets_count);
211+
while (true) {
212+
auto node = prev->load();
213+
if (!node)
214+
break;
215+
if (Equal()(node->m_key, key)) {
216+
*prev = node->m_next;
217+
return true;
218+
}
219+
prev = &node->m_next;
220+
}
221+
}
222+
return false;
223+
});
224+
}
225+
226+
#ifndef NDEBUG
227+
template <class Key, class Mapped, class Hash, class Equal>
228+
std::atomic<size_t> hash_map_tm<Key, Mapped, Hash, Equal>::s_live_nodes = 0;
229+
#endif
230+
231+
} // namespace testing

internals/include/testing/memory.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include "trade_v1/trade.hpp"
4+
5+
namespace testing {
6+
7+
template <class Object> class unique {};
8+
9+
template <class Object> class shared {};
10+
11+
} // namespace testing
12+
13+
namespace trade_v1 {
14+
15+
using namespace testing;
16+
17+
template <class Object> class atom<unique<Object>> {
18+
19+
shared<Object> load() const;
20+
};
21+
22+
} // namespace trade_v1

internals/testing/contention_test.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ using namespace trade_v1;
1313

1414
auto contention_test = test([]() {
1515
const size_t n_threads = std::thread::hardware_concurrency();
16-
const size_t n_ops = 100000;
16+
const size_t n_ops = 1000000;
1717

1818
atom<size_t> n_threads_started = 0, n_threads_stopped = 0;
1919

20-
constexpr size_t n_atoms = 7;
20+
constexpr size_t n_atoms = 7000;
2121

2222
std::unique_ptr<atom<int>[]> atoms(new atom<int>[n_atoms]);
2323
for (size_t i = 0; i < n_atoms; ++i)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#include "testing/hash_map_tm.hpp"
2+
3+
#include "testing_v1/test.hpp"
4+
5+
#include "dumpster_v1/ranqd1.hpp"
6+
7+
#include "polyfill_v1/memory.hpp"
8+
#include <thread>
9+
10+
using namespace testing_v1;
11+
12+
using namespace testing;
13+
using namespace trade;
14+
15+
auto hash_map_test = test([]() {
16+
const size_t n_threads = std::thread::hardware_concurrency();
17+
const size_t n_ops = 100000;
18+
const uint32_t max_keys = 31;
19+
20+
using hash_map_tm_type = hash_map_tm<uint32_t, size_t>;
21+
22+
hash_map_tm_type map;
23+
24+
atom<size_t> done(0);
25+
26+
auto start = std::chrono::high_resolution_clock::now();
27+
28+
for (size_t t = 0; t < n_threads; ++t)
29+
std::thread([&, t]() {
30+
auto s = static_cast<uint32_t>(t);
31+
32+
for (size_t i = 0; i < n_ops; ++i) {
33+
uint32_t key = (s = dumpster::ranqd1(s)) % max_keys;
34+
map.add_or_set(key, t, trade::stack<8192>);
35+
}
36+
37+
atomically([&]() { done.ref() += 1; });
38+
}).detach();
39+
40+
atomically(assume_readonly, [&]() {
41+
if (done != n_threads)
42+
retry();
43+
});
44+
45+
std::chrono::duration<double> elapsed =
46+
std::chrono::high_resolution_clock::now() - start;
47+
auto n_total = n_ops * n_threads;
48+
fprintf(stderr,
49+
"%f Mops in %f s = %f Mops/s\n",
50+
n_total / 1000000.0,
51+
elapsed.count(),
52+
n_total / elapsed.count() / 1000000.0);
53+
54+
verify(map.size() == max_keys);
55+
56+
{
57+
hash_map_tm_type other;
58+
map.swap(other);
59+
verify(other.size() == max_keys);
60+
verify(map.size() == 0);
61+
other.clear();
62+
}
63+
64+
#ifndef NDEBUG
65+
verify(!hash_map_tm_type::s_live_nodes);
66+
#endif
67+
});

0 commit comments

Comments
 (0)