/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/concurrency/ConcurrentHashMap.h>
#include <atomic>
#include <limits>
#include <memory>
#include <thread>
#include <vector>
#include <folly/Traits.h>
#include <folly/container/test/TrackingTypes.h>
#include <folly/hash/Hash.h>
#include <folly/portability/GFlags.h>
#include <folly/portability/GTest.h>
#include <folly/synchronization/Latch.h>
#include <folly/test/DeterministicSchedule.h>
using namespace folly::test;
using namespace folly;
using namespace std;
DEFINE_int64(seed, 0, "Seed for random number generators");
template <typename T>
class ConcurrentHashMapTest : public ::testing::Test {};
TYPED_TEST_SUITE_P(ConcurrentHashMapTest);
template <template <
typename,
typename,
uint8_t,
typename,
typename,
typename,
template <typename>
class,
class>
class Impl>
struct MapFactory {
template <
typename KeyType,
typename ValueType,
typename HashFn = std::hash<KeyType>,
typename KeyEqual = std::equal_to<KeyType>,
typename Allocator = std::allocator<uint8_t>,
uint8_t ShardBits = 8,
template <typename> class Atom = std::atomic,
class Mutex = std::mutex>
using MapT = ConcurrentHashMap<
KeyType,
ValueType,
HashFn,
KeyEqual,
Allocator,
ShardBits,
Atom,
Mutex,
Impl>;
};
#define CHM typename TypeParam::template MapT
TYPED_TEST_P(ConcurrentHashMapTest, MapTest) {
CHM<uint64_t, uint64_t> foomap(3);
EXPECT_TRUE(foomap.empty());
EXPECT_EQ(foomap.find(1), foomap.cend());
auto r = foomap.insert(1, 0);
EXPECT_TRUE(r.second);
auto r2 = foomap.insert(1, 0);
EXPECT_EQ(r.first->second, 0);
EXPECT_EQ(r.first->first, 1);
EXPECT_EQ(r2.first->second, 0);
EXPECT_EQ(r2.first->first, 1);
EXPECT_EQ(r.first, r2.first);
EXPECT_TRUE(r.second);
EXPECT_FALSE(r2.second);
EXPECT_FALSE(foomap.empty());
EXPECT_TRUE(foomap.insert(std::make_pair(2, 0)).second);
EXPECT_TRUE(foomap.insert_or_assign(2, 0).second);
EXPECT_EQ(foomap.size(), 2);
EXPECT_TRUE(foomap.assign_if_equal(2, 0, 3));
EXPECT_FALSE(foomap.assign_if(2, 4, [](auto&&) { return false; }));
EXPECT_TRUE(foomap.insert(3, 0).second);
EXPECT_FALSE(foomap.erase_if_equal(3, 1));
EXPECT_TRUE(foomap.erase_if_equal(3, 0));
EXPECT_TRUE(foomap.insert(3, 0).second);
EXPECT_NE(foomap.find(1), foomap.cend());
EXPECT_NE(foomap.find(2), foomap.cend());
EXPECT_EQ(foomap.find(2)->second, 3);
EXPECT_EQ(foomap[2], 3);
EXPECT_EQ(foomap[20], 0);
EXPECT_EQ(foomap.at(20), 0);
EXPECT_FALSE(foomap.insert(1, 0).second);
auto l = foomap.find(1);
foomap.erase(l);
EXPECT_FALSE(foomap.erase(1));
EXPECT_EQ(foomap.find(1), foomap.cend());
auto res = foomap.find(2);
EXPECT_NE(res, foomap.cend());
EXPECT_EQ(3, res->second);
EXPECT_FALSE(foomap.empty());
foomap.clear();
EXPECT_TRUE(foomap.empty());
EXPECT_TRUE(foomap.insert(3, 0).second);
EXPECT_FALSE(foomap.empty());
foomap.erase(3);
EXPECT_TRUE(foomap.empty());
}
TYPED_TEST_P(ConcurrentHashMapTest, MaxSizeTest) {
CHM<uint64_t, uint64_t> foomap(2, 16);
bool insert_failed = false;
for (int i = 0; i < 32; i++) {
auto res = foomap.insert(0, 0);
if (!res.second) {
insert_failed = true;
}
}
EXPECT_TRUE(insert_failed);
}
TYPED_TEST_P(ConcurrentHashMapTest, MoveTest) {
CHM<uint64_t, uint64_t> foomap(2, 16);
auto other = std::move(foomap);
auto other2 = std::move(other);
other = std::move(other2);
}
struct foo {
static int moved;
static int copied;
foo(foo&&) noexcept { moved++; }
foo& operator=(foo&&) {
moved++;
return *this;
}
foo& operator=(const foo&) {
copied++;
return *this;
}
foo(const foo&) { copied++; }
foo() {}
};
int foo::moved{0};
int foo::copied{0};
TYPED_TEST_P(ConcurrentHashMapTest, EmplaceTest) {
CHM<uint64_t, foo> foomap(200);
foo bar; // Make sure to test copy
foomap.insert(1, bar);
EXPECT_EQ(foo::moved, 0);
EXPECT_EQ(foo::copied, 1);
foo::copied = 0;
// The difference between emplace and try_emplace:
// If insertion fails, try_emplace does not move its argument
foomap.try_emplace(1, foo());
EXPECT_EQ(foo::moved, 0);
EXPECT_EQ(foo::copied, 0);
foomap.emplace(1, foo());
EXPECT_EQ(foo::moved, 1);
EXPECT_EQ(foo::copied, 0);
// Reset the counters. Repeated tests are allowed
foo::moved = 0;
foo::copied = 0;
}
TYPED_TEST_P(ConcurrentHashMapTest, MapInsertIteratorValueTest) {
CHM<uint64_t, uint64_t> foomap(2);
for (uint64_t i = 0; i < 1 << 16; i++) {
auto ret = foomap.insert(i, i + 1);
EXPECT_TRUE(ret.second);
EXPECT_EQ(ret.first->second, i + 1);
}
}
TYPED_TEST_P(ConcurrentHashMapTest, MapResizeTest) {
CHM<uint64_t, uint64_t> foomap(2);
EXPECT_EQ(foomap.find(1), foomap.cend());
EXPECT_TRUE(foomap.insert(1, 0).second);
EXPECT_TRUE(foomap.insert(2, 0).second);
EXPECT_TRUE(foomap.insert(3, 0).second);
EXPECT_TRUE(foomap.insert(4, 0).second);
foomap.reserve(512);
EXPECT_NE(foomap.find(1), foomap.cend());
EXPECT_NE(foomap.find(2), foomap.cend());
EXPECT_FALSE(foomap.insert(1, 0).second);
EXPECT_TRUE(foomap.erase(1));
EXPECT_EQ(foomap.find(1), foomap.cend());
auto res = foomap.find(2);
EXPECT_NE(res, foomap.cend());
if (res != foomap.cend()) {
EXPECT_EQ(0, res->second);
}
foomap.reserve(0);
}
TYPED_TEST_P(ConcurrentHashMapTest, ReserveTest) {
CHM<uint64_t, uint64_t> foomap;
int64_t insert_count = 0;
int64_t actual_count = 0;
for (int64_t i = 0; i < 1000; i++) {
if (i % 100 == 0) {
foomap.reserve(i + 100);
}
foomap.insert(std::make_pair(i, i));
insert_count++;
}
for (auto it = foomap.begin(); it != foomap.cend(); ++it) {
actual_count++;
}
EXPECT_EQ(insert_count, actual_count);
}
// Ensure we can insert objects without copy constructors.
TYPED_TEST_P(ConcurrentHashMapTest, MapNoCopiesTest) {
struct Uncopyable {
int i_;
Uncopyable(int i) { i_ = i; }
Uncopyable(const Uncopyable& that) = delete;
bool operator==(const Uncopyable& o) const { return i_ == o.i_; }
};
struct Hasher {
size_t operator()(const Uncopyable&) const { return 0; }
};
CHM<Uncopyable, Uncopyable, Hasher> foomap(2);
EXPECT_TRUE(foomap.try_emplace(1, 1).second);
EXPECT_TRUE(foomap.try_emplace(2, 2).second);
auto res = foomap.find(2);
EXPECT_NE(res, foomap.cend());
EXPECT_TRUE(foomap.try_emplace(3, 3).second);
auto res2 = foomap.find(2);
EXPECT_NE(res2, foomap.cend());
EXPECT_EQ(&(res->second), &(res2->second));
}
TYPED_TEST_P(ConcurrentHashMapTest, MapMovableKeysTest) {
struct Movable {
int i_;
Movable(int i) { i_ = i; }
Movable(const Movable&) = delete;
Movable(Movable&& o) {
i_ = o.i_;
o.i_ = 0;
}
bool operator==(const Movable& o) const { return i_ == o.i_; }
};
struct Hasher {
size_t operator()(const Movable&) const { return 0; }
};
CHM<Movable, Movable, Hasher> foomap(2);
EXPECT_TRUE(foomap.insert(std::make_pair(Movable(10), Movable(1))).second);
EXPECT_TRUE(foomap.assign(Movable(10), Movable(2)));
EXPECT_TRUE(foomap.insert(Movable(11), Movable(1)).second);
EXPECT_TRUE(foomap.emplace(Movable(12), Movable(1)).second);
EXPECT_TRUE(foomap.insert_or_assign(Movable(10), Movable(3)).second);
EXPECT_TRUE(foomap.assign_if_equal(Movable(10), Movable(3), Movable(4)));
EXPECT_TRUE(
foomap.assign_if(Movable(10), Movable(5), [](auto&&) { return true; }));
EXPECT_FALSE(foomap.try_emplace(Movable(10), Movable(3)).second);
EXPECT_TRUE(foomap.try_emplace(Movable(13), Movable(3)).second);
}
TYPED_TEST_P(ConcurrentHashMapTest, MapUpdateTest) {
CHM<uint64_t, uint64_t> foomap(2);
EXPECT_TRUE(foomap.insert(1, 10).second);
EXPECT_TRUE(bool(foomap.assign(1, 11)));
auto res = foomap.find(1);
EXPECT_NE(res, foomap.cend());
EXPECT_EQ(11, res->second);
}
TYPED_TEST_P(ConcurrentHashMapTest, MapIterateTest2) {
CHM<uint64_t, uint64_t> foomap(2);
auto begin = foomap.cbegin();
auto end = foomap.cend();
EXPECT_EQ(begin, end);
}
TYPED_TEST_P(ConcurrentHashMapTest, MapIterateTest) {
CHM<uint64_t, uint64_t> foomap(2);
EXPECT_EQ(foomap.cbegin(), foomap.cend());
EXPECT_TRUE(foomap.insert(1, 1).second);
EXPECT_TRUE(foomap.insert(2, 2).second);
auto iter = foomap.cbegin();
EXPECT_NE(iter, foomap.cend());
EXPECT_EQ(iter->first, 1);
EXPECT_EQ(iter->second, 1);
++iter;
EXPECT_NE(iter, foomap.cend());
EXPECT_EQ(iter->first, 2);
EXPECT_EQ(iter->second, 2);
++iter;
EXPECT_EQ(iter, foomap.cend());
int count = 0;
for (auto it = foomap.cbegin(); it != foomap.cend(); ++it) {
count++;
}
EXPECT_EQ(count, 2);
}
TYPED_TEST_P(ConcurrentHashMapTest, MoveIterateAssignIterate) {
using Map = CHM<int, int>;
Map tmp;
Map map{std::move(tmp)};
map.insert(0, 0);
++map.cbegin();
CHM<int, int> other;
other.insert(0, 0);
map = std::move(other);
++map.cbegin();
}
TYPED_TEST_P(ConcurrentHashMapTest, EraseTest) {
CHM<uint64_t, uint64_t> foomap(3);
foomap.insert(1, 0);
auto f1 = foomap.find(1);
EXPECT_EQ(1, foomap.erase(1));
foomap.erase(f1);
}
TYPED_TEST_P(ConcurrentHashMapTest, EraseIfEqualTest) {
CHM<uint64_t, uint64_t> foomap(3);
foomap.insert(1, 0);
EXPECT_FALSE(foomap.erase_if_equal(1, 1));
auto f1 = foomap.find(1);
EXPECT_EQ(0, f1->second);
EXPECT_TRUE(foomap.erase_if_equal(1, 0));
EXPECT_EQ(foomap.find(1), foomap.cend());
}
TYPED_TEST_P(ConcurrentHashMapTest, EraseIfTest) {
CHM<uint64_t, uint64_t> foomap(3);
foomap.insert(1, 0);
EXPECT_FALSE(
foomap.erase_key_if(1, [](const uint64_t& value) { return value == 1; }));
auto f1 = foomap.find(1);
EXPECT_EQ(0, f1->second);
EXPECT_TRUE(
foomap.erase_key_if(1, [](const uint64_t& value) { return value == 0; }));
EXPECT_EQ(foomap.find(1), foomap.cend());
CHM<std::string, std::weak_ptr<uint64_t>> barmap(3);
auto shared = std::make_shared<uint64_t>(123);
barmap.insert("test", shared);
EXPECT_FALSE(barmap.erase_key_if(
"test",
[](const std::weak_ptr<uint64_t>& ptr) { return ptr.expired(); }));
EXPECT_EQ(*barmap.find("test")->second.lock(), 123);
shared.reset();
EXPECT_TRUE(barmap.erase_key_if(
"test",
[](const std::weak_ptr<uint64_t>& ptr) { return ptr.expired(); }));
EXPECT_EQ(barmap.find("test"), barmap.cend());
}
TYPED_TEST_P(ConcurrentHashMapTest, CopyIterator) {
CHM<int, int> map;
map.insert(0, 0);
for (auto cit = map.cbegin(); cit != map.cend(); ++cit) {
std::pair<int const, int> const ckv{0, 0};
EXPECT_EQ(*cit, ckv);
}
}
TYPED_TEST_P(ConcurrentHashMapTest, EraseInIterateTest) {
CHM<uint64_t, uint64_t> foomap(3);
for (uint64_t k = 0; k < 10; ++k) {
foomap.insert(k, k);
}
EXPECT_EQ(10, foomap.size());
for (auto it = foomap.cbegin(); it != foomap.cend();) {
if (it->second > 3) {
it = foomap.erase(it);
} else {
++it;
}
}
EXPECT_EQ(4, foomap.size());
for (auto it = foomap.cbegin(); it != foomap.cend(); ++it) {
EXPECT_GE(3, it->second);
}
}
TYPED_TEST_P(ConcurrentHashMapTest, AssignIfTest) {
CHM<uint64_t, uint64_t> foomap(3);
foomap.insert(1, 0);
bool canAssignFlag = false;
EXPECT_FALSE(
foomap.assign_if(1, 1, [canAssignFlag](auto&&) { return canAssignFlag; })
.has_value());
canAssignFlag = true;
auto f1 =
foomap.assign_if(1, 2, [canAssignFlag](auto&&) { return canAssignFlag; });
EXPECT_TRUE(f1.has_value());
EXPECT_EQ(2, f1.value()->second);
// Assign based on the current value.
auto f2 = foomap.assign_if(1, 3, [](auto&& val) { return val == 2; });
EXPECT_TRUE(f2.has_value());
EXPECT_EQ(3, f2.value()->second);
}
// TODO: hazptrs must support DeterministicSchedule
#define Atom std::atomic // DeterministicAtomic
#define Mutex std::mutex // DeterministicMutex
#define lib std // DeterministicSchedule
#define join t.join() // DeterministicSchedule::join(t)
// #define Atom DeterministicAtomic
// #define Mutex DeterministicMutex
// #define lib DeterministicSchedule
// #define join DeterministicSchedule::join(t)
TYPED_TEST_P(ConcurrentHashMapTest, UpdateStressTest) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
// size must match iters for this test.
unsigned size = 128 * 128;
unsigned iters = size;
CHM<unsigned long,
unsigned long,
std::hash<unsigned long>,
std::equal_to<unsigned long>,
std::allocator<uint8_t>,
8,
Atom,
Mutex>
m(2);
for (uint32_t i = 0; i < size; i++) {
m.insert(i, i);
}
std::vector<std::thread> threads;
unsigned int num_threads = 32;
threads.reserve(num_threads);
for (uint32_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([&, t]() {
int offset = (iters * t / num_threads);
for (uint32_t i = 0; i < iters / num_threads; i++) {
unsigned long k = folly::hash::jenkins_rev_mix32((i + offset));
k = k % (iters / num_threads) + offset;
unsigned long val = 3;
{
auto res = m.find(k);
EXPECT_NE(res, m.cend());
EXPECT_EQ(k, res->second);
auto r = m.assign(k, res->second);
EXPECT_TRUE(r);
}
{
auto res = m.find(k);
EXPECT_NE(res, m.cend());
EXPECT_EQ(k, res->second);
}
// Another random insertion to force table resizes
val = size + i + offset;
EXPECT_TRUE(m.insert(val, val).second);
}
}));
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, EraseStressTest) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
unsigned size = 2;
unsigned iters = size * 128 * 2;
CHM<unsigned long,
unsigned long,
std::hash<unsigned long>,
std::equal_to<unsigned long>,
std::allocator<uint8_t>,
8,
Atom,
Mutex>
m(2);
for (uint32_t i = 0; i < size; i++) {
unsigned long k = folly::hash::jenkins_rev_mix32(i);
m.insert(k, k);
}
std::vector<std::thread> threads;
unsigned int num_threads = 32;
threads.reserve(num_threads);
for (uint32_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([&, t]() {
int offset = (iters * t / num_threads);
for (uint32_t i = 0; i < iters / num_threads; i++) {
unsigned long k = folly::hash::jenkins_rev_mix32((i + offset));
auto res = m.insert(k, k).second;
if (res) {
if (i % 2 == 0) {
res = m.erase(k);
} else {
res = m.erase_if_equal(k, k);
}
if (!res) {
printf("Faulre to erase thread %i val %li\n", t, k);
exit(0);
}
EXPECT_TRUE(res);
}
res = m.insert(k, k).second;
if (res) {
res = bool(m.assign(k, k));
if (!res) {
printf("Thread %i update fail %li res%i\n", t, k, res);
exit(0);
}
EXPECT_TRUE(res);
auto result = m.find(k);
if (result == m.cend()) {
printf("Thread %i lookup fail %li\n", t, k);
exit(0);
}
EXPECT_EQ(k, result->second);
}
}
}));
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, TryEmplaceEraseStressTest) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
std::atomic<int> iterations{10000};
std::vector<std::thread> threads;
unsigned int num_threads = 32;
threads.reserve(num_threads);
CHM<int, int> map;
for (uint32_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([&]() {
while (--iterations >= 0) {
auto it = map.try_emplace(1, 101);
map.erase(1);
EXPECT_EQ(it.first->second, 101);
}
}));
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, InsertOrAssignStressTest) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
std::atomic<int> iterations{10000};
std::vector<std::thread> threads;
unsigned int num_threads = 32;
threads.reserve(num_threads);
CHM<int, int> map;
for (uint32_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([&]() {
int i = 0;
while (--iterations >= 0) {
auto res = map.insert_or_assign(0, ++i);
ASSERT_TRUE(res.second);
auto v = res.first->second;
ASSERT_EQ(v, i);
}
}));
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, IterateStressTest) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
unsigned size = 2;
unsigned iters = size * 128 * 2;
CHM<unsigned long,
unsigned long,
std::hash<unsigned long>,
std::equal_to<unsigned long>,
std::allocator<uint8_t>,
8,
Atom,
Mutex>
m(2);
for (uint32_t i = 0; i < size; i++) {
unsigned long k = folly::hash::jenkins_rev_mix32(i);
m.insert(k, k);
}
for (uint32_t i = 0; i < 10; i++) {
m.insert(i, i);
}
std::vector<std::thread> threads;
unsigned int num_threads = 32;
for (uint32_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([&, t]() {
int offset = (iters * t / num_threads);
for (uint32_t i = 0; i < iters / num_threads; i++) {
unsigned long k = folly::hash::jenkins_rev_mix32((i + offset));
auto res = m.insert(k, k).second;
if (res) {
if (i % 2 == 0) {
res = m.erase(k);
} else {
res = m.erase_if_equal(k, k);
}
if (!res) {
printf("Faulre to erase thread %i val %li\n", t, k);
exit(0);
}
EXPECT_TRUE(res);
}
int count = 0;
for (auto it = m.cbegin(); it != m.cend(); ++it) {
printf("Item is %li\n", it->first);
if (it->first < 10) {
count++;
}
}
EXPECT_EQ(count, 10);
}
}));
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, insertStressTest) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
unsigned size = 2;
unsigned iters = size * 64 * 4;
CHM<unsigned long,
unsigned long,
std::hash<unsigned long>,
std::equal_to<unsigned long>,
std::allocator<uint8_t>,
8,
Atom,
Mutex>
m(2);
EXPECT_TRUE(m.insert(0, 0).second);
EXPECT_FALSE(m.insert(0, 0).second);
std::vector<std::thread> threads;
unsigned int num_threads = 32;
for (uint32_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([&, t]() {
int offset = (iters * t / num_threads);
for (uint32_t i = 0; i < iters / num_threads; i++) {
auto var = offset + i + 1;
EXPECT_TRUE(m.insert(var, var).second);
EXPECT_FALSE(m.insert(0, 0).second);
}
}));
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, assignStressTest) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
unsigned size = 2;
unsigned iters = size * 64 * 4;
struct big_value {
uint64_t v1;
uint64_t v2;
uint64_t v3;
uint64_t v4;
uint64_t v5;
uint64_t v6;
uint64_t v7;
uint64_t v8;
void set(uint64_t v) { v1 = v2 = v3 = v4 = v5 = v6 = v7 = v8 = v; }
void check() const {
auto v = v1;
EXPECT_EQ(v, v8);
EXPECT_EQ(v, v7);
EXPECT_EQ(v, v6);
EXPECT_EQ(v, v5);
EXPECT_EQ(v, v4);
EXPECT_EQ(v, v3);
EXPECT_EQ(v, v2);
}
};
CHM<unsigned long,
big_value,
std::hash<unsigned long>,
std::equal_to<unsigned long>,
std::allocator<uint8_t>,
8,
Atom,
Mutex>
m(2);
for (uint32_t i = 0; i < iters; i++) {
big_value a;
a.set(i);
m.insert(i, a);
}
std::vector<std::thread> threads;
unsigned int num_threads = 32;
for (uint32_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([&]() {
for (uint32_t i = 0; i < iters; i++) {
auto res = m.find(i);
EXPECT_NE(res, m.cend());
res->second.check();
big_value b;
b.set(res->second.v1 + 1);
m.assign(i, b);
}
}));
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, RefcountTest) {
struct badhash {
size_t operator()(uint64_t) const { return 0; }
};
CHM<uint64_t,
uint64_t,
badhash,
std::equal_to<uint64_t>,
std::allocator<uint8_t>,
0>
foomap(3);
foomap.insert(0, 0);
foomap.insert(1, 1);
foomap.insert(2, 2);
for (int32_t i = 0; i < 300; ++i) {
foomap.insert_or_assign(1, i);
}
}
struct Wrapper {
explicit Wrapper(bool& del_) : del(del_) {}
~Wrapper() { del = true; }
bool& del;
};
TYPED_TEST_P(ConcurrentHashMapTest, Deletion) {
bool del{false};
{
CHM<int, std::shared_ptr<Wrapper>> map;
map.insert(0, std::make_shared<Wrapper>(del));
}
folly::hazptr_cleanup();
EXPECT_TRUE(del);
}
TYPED_TEST_P(ConcurrentHashMapTest, DeletionWithErase) {
bool del{false};
{
CHM<int, std::shared_ptr<Wrapper>> map;
map.insert(0, std::make_shared<Wrapper>(del));
map.erase(0);
}
folly::hazptr_cleanup();
EXPECT_TRUE(del);
}
TYPED_TEST_P(ConcurrentHashMapTest, DeletionWithIterator) {
bool del{false};
{
CHM<int, std::shared_ptr<Wrapper>> map;
map.insert(0, std::make_shared<Wrapper>(del));
auto it = map.find(0);
map.erase(it);
}
folly::hazptr_cleanup();
EXPECT_TRUE(del);
}
TYPED_TEST_P(ConcurrentHashMapTest, DeletionWithForLoop) {
bool del{false};
{
CHM<int, std::shared_ptr<Wrapper>> map;
map.insert(0, std::make_shared<Wrapper>(del));
for (auto it = map.cbegin(); it != map.cend(); ++it) {
EXPECT_EQ(it->first, 0);
}
}
folly::hazptr_cleanup();
EXPECT_TRUE(del);
}
TYPED_TEST_P(ConcurrentHashMapTest, DeletionMultiple) {
bool del1{false}, del2{false};
{
CHM<int, std::shared_ptr<Wrapper>> map;
map.insert(0, std::make_shared<Wrapper>(del1));
map.insert(1, std::make_shared<Wrapper>(del2));
}
folly::hazptr_cleanup();
EXPECT_TRUE(del1);
EXPECT_TRUE(del2);
}
TYPED_TEST_P(ConcurrentHashMapTest, DeletionAssigned) {
bool del1{false}, del2{false};
{
CHM<int, std::shared_ptr<Wrapper>> map;
map.insert(0, std::make_shared<Wrapper>(del1));
map.insert_or_assign(0, std::make_shared<Wrapper>(del2));
}
folly::hazptr_cleanup();
EXPECT_TRUE(del1);
EXPECT_TRUE(del2);
}
TYPED_TEST_P(ConcurrentHashMapTest, DeletionMultipleMaps) {
bool del1{false}, del2{false};
{
CHM<int, std::shared_ptr<Wrapper>> map1;
CHM<int, std::shared_ptr<Wrapper>> map2;
map1.insert(0, std::make_shared<Wrapper>(del1));
map2.insert(0, std::make_shared<Wrapper>(del2));
}
folly::hazptr_cleanup();
EXPECT_TRUE(del1);
EXPECT_TRUE(del2);
}
TYPED_TEST_P(ConcurrentHashMapTest, ForEachLoop) {
CHM<int, int> map;
map.insert(1, 2);
size_t iters = 0;
for (const auto& kv : map) {
EXPECT_EQ(kv.first, 1);
EXPECT_EQ(kv.second, 2);
++iters;
}
EXPECT_EQ(iters, 1);
}
template <typename T>
struct FooBase {
typename T::ConstIterator it;
explicit FooBase(typename T::ConstIterator&& it_) : it(std::move(it_)) {}
FooBase(FooBase&&) = default;
FooBase& operator=(FooBase&&) = default;
};
TYPED_TEST_P(ConcurrentHashMapTest, IteratorMove) {
using Foo = FooBase<CHM<int, int>>;
CHM<int, int> map;
int k = 111;
int v = 999999;
map.insert(k, v);
Foo foo(map.find(k));
ASSERT_EQ(foo.it->second, v);
Foo foo2(map.find(0));
foo2 = std::move(foo);
ASSERT_EQ(foo2.it->second, v);
}
TYPED_TEST_P(ConcurrentHashMapTest, IteratorLoop) {
CHM<std::string, int> map;
static constexpr size_t kNum = 4000;
for (size_t i = 0; i < kNum; ++i) {
map.insert(to<std::string>(i), i);
}
size_t count = 0;
for (auto it = map.begin(); it != map.end(); ++it) {
ASSERT_LT(count, kNum);
++count;
}
EXPECT_EQ(count, kNum);
}
namespace {
template <typename T, typename Arg>
using detector_find = decltype(std::declval<T>().find(std::declval<Arg>()));
template <typename T, typename Arg>
using detector_erase = decltype(std::declval<T>().erase(std::declval<Arg>()));
} // namespace
TYPED_TEST_P(ConcurrentHashMapTest, HeterogeneousLookup) {
using Hasher = folly::transparent<folly::hasher<folly::StringPiece>>;
using KeyEqual = folly::transparent<std::equal_to<folly::StringPiece>>;
using M = CHM<std::string, bool, Hasher, KeyEqual>;
constexpr auto hello = "hello"_sp;
constexpr auto buddy = "buddy"_sp;
constexpr auto world = "world"_sp;
M map;
map.emplace(hello, true);
map.emplace(world, false);
auto checks = [hello, buddy](auto& ref) {
// find
EXPECT_TRUE(ref.end() == ref.find(buddy));
EXPECT_EQ(hello, ref.find(hello)->first);
// at
EXPECT_TRUE(ref.at(hello));
EXPECT_THROW(ref.at(buddy), std::out_of_range);
// invocability checks
static_assert(
!is_detected_v<detector_find, decltype(ref), int>,
"there shouldn't be a find() overload for this string map with an int param");
};
checks(map);
checks(std::as_const(map));
}
TYPED_TEST_P(ConcurrentHashMapTest, HeterogeneousInsert) {
using Hasher = folly::transparent<folly::hasher<folly::StringPiece>>;
using KeyEqual = folly::transparent<std::equal_to<folly::StringPiece>>;
using P = std::pair<StringPiece, std::string>;
using CP = std::pair<const StringPiece, std::string>;
CHM<std::string, std::string, Hasher, KeyEqual> map;
P p{"foo", "hello"};
StringPiece foo{"foo"};
StringPiece bar{"bar"};
map.insert("foo", "hello");
map.insert(foo, "hello");
// TODO(T31574848): the list-initialization below does not work on libstdc++
// versions (e.g., GCC < 6) with no implementation of N4387 ("perfect
// initialization" for pairs and tuples).
// StringPiece sp{"foo"};
// map.insert({sp, "hello"});
map.insert({"foo", "hello"});
map.insert(P("foo", "hello"));
map.insert(CP("foo", "hello"));
map.insert(std::move(p));
map.insert_or_assign("foo", "hello");
map.insert_or_assign(StringPiece{"foo"}, "hello");
map.erase(StringPiece{"foo"});
map.erase(foo);
map.erase("");
EXPECT_TRUE(map.empty());
map.insert("foo", "hello");
map.insert("bar", "world");
map.erase_if_equal(StringPiece{"foo"}, "hello");
map.erase_key_if(bar, [](const std::string& s) { return s == "world"; });
map.erase("");
EXPECT_TRUE(map.empty());
map.insert("foo", "baz");
EXPECT_TRUE(map.assign(foo, "hello2"));
auto mbIt = map.assign_if_equal("foo", "hello2", "hello");
EXPECT_TRUE(mbIt);
EXPECT_EQ(mbIt.value()->second, "hello");
EXPECT_EQ(map[foo], "hello");
auto mbIt2 =
map.assign_if("foo", "hello2", [](auto&& val) { return val == "hello"; });
EXPECT_TRUE(mbIt);
EXPECT_EQ(mbIt2.value()->second, "hello2");
EXPECT_EQ(map[foo], "hello2");
auto it = map.find(foo);
map.erase(it);
EXPECT_TRUE(map.empty());
map.try_emplace(foo);
map.try_emplace(foo, "hello");
map.try_emplace(StringPiece{"foo"}, "hello");
map.try_emplace(foo, "hello");
map.try_emplace(foo);
map.try_emplace("foo");
map.try_emplace("foo", "hello");
map.try_emplace("bar", /* count */ 20, 'x');
EXPECT_EQ(map[bar], std::string(20, 'x'));
map.emplace(StringPiece{"foo"}, "hello");
map.emplace("foo", "hello");
// invocability checks
static_assert(
!is_detected_v<detector_erase, decltype(map), int>,
"there shouldn't be an erase() overload for this string map with an int param");
}
TYPED_TEST_P(ConcurrentHashMapTest, InsertOrAssignIterator) {
CHM<int, int> map;
auto [itr1, insert1] = map.insert_or_assign(1, 1);
auto [itr2, insert2] = map.insert_or_assign(1, 2);
auto itr3 = map.find(1);
EXPECT_EQ(itr3->second, 2);
EXPECT_EQ(itr2->second, 2);
}
TYPED_TEST_P(ConcurrentHashMapTest, EraseClonedNonCopyable) {
// Using a non-copyable value type to use the node structure with an
// extra level of indirection to key-value items.
using Value = std::unique_ptr<int>;
// [TODO] Fix the SIMD version to pass this test, then change the
// map type to CHM.
ConcurrentHashMap<int, Value> map;
int cloned = 32; // The item that will end up being cloned.
for (int i = 0; i < cloned; i++) {
map.try_emplace(256 * i, std::make_unique<int>(0));
}
auto [iter, _] = map.try_emplace(256 * cloned, std::make_unique<int>(0));
// Add more items to cause rehash that clones the item.
int num = 10000;
for (int i = cloned + 1; i < num; i++) {
map.try_emplace(256 * i, std::make_unique<int>(0));
}
// Erase items to invoke hazard pointer asynchronous reclamation.
for (int i = 0; i < num; i++) {
map.erase(256 * i);
}
// The cloned node and the associated key-value item should still be
// protected by iter from being reclaimed.
EXPECT_EQ(iter->first, 256 * cloned);
}
TYPED_TEST_P(ConcurrentHashMapTest, ConcurrentInsertClear) {
DeterministicSchedule sched(DeterministicSchedule::uniform(FLAGS_seed));
/* 8192 and 8x / 9x multipliers are values that tend to reproduce
* race condition (fixed by this change) more frequently.
* Test keeps CHM size limited to chm_max_size and clear() if it exceeds.
* Key space for CHM is limited to chm_max_key and exceeds max size by
* small margin.
* Test imitates serial traversal over key space by multiple threads,
* triggering clear() by multiple threads at once.
*/
constexpr unsigned long chm_base_size = 8192;
constexpr unsigned long chm_max_size = chm_base_size * 8;
constexpr unsigned long chm_max_key = chm_base_size * 9;
CHM<unsigned long,
unsigned long,
std::hash<unsigned long>,
std::equal_to<unsigned long>,
std::allocator<uint8_t>,
8,
Atom,
Mutex>
m(chm_base_size);
std::vector<std::thread> threads;
/* 32 threads and 50k rounds are a compromise between trying to create
* race conditions in insert()/clear(), and finishing test in reasonable
* time */
constexpr int load_divisor = folly::kIsSanitizeThread ? 4 : 1;
constexpr size_t num_threads = 32 / load_divisor;
constexpr size_t rounds_per_thread = 50000 / load_divisor;
threads.reserve(num_threads);
for (size_t t = 0; t < num_threads; t++) {
threads.emplace_back([&, t]() {
for (size_t i = 0; i < rounds_per_thread; ++i) {
/* Code simulates traversal over whole key space by all threads
* combined, with each thread contributing to semi-unique set of keys.
* Each thread is assigned its own key sequence,
* with thread_X traversing values X, 32+X, 32*2+X, 32*3+X, ...
* 32*N+X mod chm_max_key.
*/
const unsigned long k = (i * num_threads + t) % chm_max_key;
if (m.size() >= chm_max_size) {
m.clear();
}
m.insert_or_assign(k, k);
}
});
}
for (auto& t : threads) {
join;
}
}
TYPED_TEST_P(ConcurrentHashMapTest, StressTestReclamation) {
// Create a map where we keep reclaiming a lot of objects that are linked to
// one node.
// Ensure all entries are mapped to a single segment.
struct constant_hash {
uint64_t operator()(unsigned long) const noexcept { return 0; }
};
CHM<unsigned long, unsigned long, constant_hash> map;
static constexpr unsigned long key_prev =
0; // A key that the test key has a link to - to guard against immediate
// reclamation.
static constexpr unsigned long key_test =
1; // A key that keeps being reclaimed repeatedly.
static constexpr unsigned long key_link_explosion =
2; // A key that is linked to the test key.
EXPECT_TRUE(map.insert(std::make_pair(key_prev, 0)).second);
EXPECT_TRUE(map.insert(std::make_pair(key_test, 0)).second);
EXPECT_TRUE(map.insert(std::make_pair(key_link_explosion, 0)).second);
std::vector<std::thread> threads;
// Test with (2^16)+ threads, enough to overflow a 16 bit integer.
// It should be uncommon to have more than 2^32 concurrent accesses.
static constexpr uint64_t num_threads = std::numeric_limits<uint16_t>::max();
static constexpr uint64_t iters = 100;
folly::Latch start(num_threads);
for (uint64_t t = 0; t < num_threads; t++) {
threads.push_back(lib::thread([t, &map, &start]() {
start.arrive_and_wait();
static constexpr uint64_t progress_report_pct =
(iters / 20); // Every 5% we log progress
for (uint64_t i = 0; i < iters; i++) {
if (t == 0 && (i % progress_report_pct) == 0) {
// To a casual observer - to know that the test is progressing, even
// if slowly
LOG(INFO) << "Progress: " << (i * 100 / iters);
}
map.insert_or_assign(key_test, i * num_threads);
}
}));
}
for (auto& t : threads) {
join;
}
}
REGISTER_TYPED_TEST_SUITE_P(
ConcurrentHashMapTest,
MapTest,
MaxSizeTest,
MoveTest,
EmplaceTest,
MapResizeTest,
ReserveTest,
MapNoCopiesTest,
MapMovableKeysTest,
MapUpdateTest,
MapIterateTest2,
MapIterateTest,
MoveIterateAssignIterate,
MapInsertIteratorValueTest,
CopyIterator,
Deletion,
DeletionAssigned,
DeletionMultiple,
DeletionMultipleMaps,
DeletionWithErase,
DeletionWithForLoop,
DeletionWithIterator,
EraseIfEqualTest,
EraseIfTest,
EraseInIterateTest,
AssignIfTest,
EraseStressTest,
EraseTest,
ForEachLoop,
TryEmplaceEraseStressTest,
InsertOrAssignStressTest,
IterateStressTest,
RefcountTest,
UpdateStressTest,
assignStressTest,
insertStressTest,
IteratorMove,
IteratorLoop,
HeterogeneousLookup,
HeterogeneousInsert,
InsertOrAssignIterator,
EraseClonedNonCopyable,
ConcurrentInsertClear,
StressTestReclamation);
using folly::detail::concurrenthashmap::bucket::BucketTable;
#if FOLLY_SSE_PREREQ(4, 2) && FOLLY_F14_VECTOR_INTRINSICS_AVAILABLE
using folly::detail::concurrenthashmap::simd::SIMDTable;
typedef ::testing::Types<MapFactory<BucketTable>, MapFactory<SIMDTable>>
MapFactoryTypes;
#else
typedef ::testing::Types<MapFactory<BucketTable>> MapFactoryTypes;
#endif
INSTANTIATE_TYPED_TEST_SUITE_P(
MapFactoryTypesInstantiation, ConcurrentHashMapTest, MapFactoryTypes);