xo-arena: DArenaHashMap try_expand bugfix + edge utest
This commit is contained in:
parent
2af28cafa9
commit
58c5319d03
4 changed files with 245 additions and 36 deletions
|
|
@ -174,12 +174,55 @@ namespace xo {
|
|||
n_slot_{group_exp2.second * c_group_size},
|
||||
control_{DArenaVector<uint8_t>::map(ArenaConfig{.size_ = n_slot_ + c_group_size})},
|
||||
slots_{DArenaVector<value_type>::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})}
|
||||
{}
|
||||
{
|
||||
/* here: arenas have allocated address range, but no committed memory yet */
|
||||
|
||||
this->_init();
|
||||
}
|
||||
|
||||
size_type empty() const noexcept { return size_ == 0; }
|
||||
size_type capacity() const noexcept { return n_group_ * c_group_size; }
|
||||
float load_factor() const noexcept { return size_ / static_cast<float>(n_slot_); }
|
||||
|
||||
void resize_from_empty(const std::pair<size_type,
|
||||
size_type> & group_exp2)
|
||||
{
|
||||
assert(size_ == 0);
|
||||
|
||||
this->n_group_exponent_ = group_exp2.first;
|
||||
this->n_group_ = group_exp2.second;
|
||||
this->n_slot_ = group_exp2.second * c_group_size;
|
||||
|
||||
this->_init();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
/* remark: discontinuity in the sense that we lose n_group_ = 2 ^ n_group_epxonent_
|
||||
*
|
||||
* juice may not be worth the squeeze here,
|
||||
* since DArena doesn't yet (Jan 2026) unmap on clear
|
||||
*/
|
||||
|
||||
this->size_ = 0;
|
||||
this->n_group_exponent_ = 0;
|
||||
this->n_group_ = 0;
|
||||
this->n_slot_ = 0;
|
||||
this->control_.resize(0);
|
||||
this->slots_.resize(0);
|
||||
}
|
||||
|
||||
public:
|
||||
void _init() {
|
||||
this->control_.resize(n_slot_ + c_group_size);
|
||||
|
||||
/* all slots marked empty initially */
|
||||
std::fill(this->control_.begin(),
|
||||
this->control_.end(),
|
||||
c_empty_slot);
|
||||
|
||||
this->slots_.resize(n_slot_);
|
||||
}
|
||||
|
||||
group_type _load_group(size_type ix) {
|
||||
return group_type(&(control_[ix]));
|
||||
}
|
||||
|
|
@ -296,6 +339,8 @@ namespace xo {
|
|||
size_type capacity() const noexcept { return store_.capacity(); }
|
||||
float load_factor() const noexcept { return store_.load_factor(); }
|
||||
|
||||
bool verify_ok(verify_policy p = verify_policy::throw_only()) const;
|
||||
|
||||
iterator begin() {
|
||||
iterator ix(&(store_.control_[0]),
|
||||
&(store_.control_[store_.capacity()]),
|
||||
|
|
@ -334,7 +379,8 @@ namespace xo {
|
|||
**/
|
||||
bool insert(const value_type & kv_pair);
|
||||
|
||||
bool verify_ok(verify_policy p = verify_policy::throw_only()) const;
|
||||
/** reset to empty state **/
|
||||
void clear();
|
||||
|
||||
private:
|
||||
/** insert @p kv_pair,
|
||||
|
|
@ -389,15 +435,6 @@ namespace xo {
|
|||
store_{lub_exp2(lub_group_mult(hint_max_capacity))},
|
||||
debug_flag_{debug_flag}
|
||||
{
|
||||
/* invariant: arenas have allocated address range, but no committed memory yet */
|
||||
this->store_.control_.resize(store_.n_slot_ + c_group_size);
|
||||
|
||||
/* all slots marked empty initially */
|
||||
std::fill(this->store_.control_.begin(),
|
||||
this->store_.control_.end(),
|
||||
c_empty_slot);
|
||||
|
||||
this->store_.slots_.resize(store_.n_slot_);
|
||||
}
|
||||
|
||||
template <typename Key, typename Value, typename Hash, typename Equal>
|
||||
|
|
@ -426,6 +463,8 @@ namespace xo {
|
|||
-> std::pair<value_type *, bool>
|
||||
|
||||
{
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
size_type h = hash_value;
|
||||
// h1: hi bits: probe sequence
|
||||
size_type h1 = h >> 7;
|
||||
|
|
@ -434,6 +473,10 @@ namespace xo {
|
|||
|
||||
size_type N = p_store->capacity();
|
||||
|
||||
if (N == 0) [[unlikely]] {
|
||||
return std::make_pair(nullptr, false);
|
||||
}
|
||||
|
||||
// same as:
|
||||
// ix = h1 % N
|
||||
// since N is power of 2
|
||||
|
|
@ -528,36 +571,58 @@ namespace xo {
|
|||
bool
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::_try_grow()
|
||||
{
|
||||
size_type n_group_exponent_2x = store_.n_group_exponent_ + 1;
|
||||
size_type n_group_2x = store_.n_group_ * 2;
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
detail::HashMapStore<Key, Value> store_2x(std::make_pair(n_group_exponent_2x,
|
||||
n_group_2x));
|
||||
/* rehash everything in store_,
|
||||
* into store_2x
|
||||
*/
|
||||
size_type n_group_exponent_2x = 0;
|
||||
size_type n_group_2x = 0;
|
||||
|
||||
for (size_type i = 0, n = store_.capacity(); i < n; ++i) {
|
||||
uint8_t ctrl = store_.control_[i];
|
||||
value_type & kv_pair = store_.slots_[i];
|
||||
|
||||
if ((ctrl != c_empty_slot)
|
||||
&& (ctrl != c_tombstone))
|
||||
{
|
||||
size_type h = hash_(kv_pair.first);
|
||||
auto chk = this->_try_insert_aux(h, kv_pair, &store_2x);
|
||||
|
||||
if (!chk.second) {
|
||||
// shenanigans - something isn't right.
|
||||
// - may have run out of memory
|
||||
assert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (store_.n_group_ == 0) [[unlikely]] {
|
||||
// special case: grow from hard empty state
|
||||
n_group_exponent_2x = 0;
|
||||
n_group_2x = 1;
|
||||
} else {
|
||||
n_group_exponent_2x = store_.n_group_exponent_ + 1;
|
||||
n_group_2x = 2 * n_group_exponent_2x;
|
||||
}
|
||||
|
||||
this->store_ = std::move(store_2x);
|
||||
// optimization when table is empty. in that case can resize
|
||||
// arenas in place
|
||||
|
||||
if (this->empty()) {
|
||||
log && log("resize-from-empty branch");
|
||||
|
||||
this->store_.resize_from_empty(std::make_pair(n_group_exponent_2x, n_group_2x));
|
||||
} else {
|
||||
log && log("duplicate-and-replace branch");
|
||||
|
||||
detail::HashMapStore<Key, Value> store_2x(std::make_pair(n_group_exponent_2x,
|
||||
n_group_2x));
|
||||
/* rehash everything in store_,
|
||||
* into store_2x
|
||||
*/
|
||||
|
||||
for (size_type i = 0, n = store_.capacity(); i < n; ++i) {
|
||||
uint8_t ctrl = store_.control_[i];
|
||||
value_type & kv_pair = store_.slots_[i];
|
||||
|
||||
if ((ctrl != c_empty_slot)
|
||||
&& (ctrl != c_tombstone))
|
||||
{
|
||||
size_type h = hash_(kv_pair.first);
|
||||
auto chk = this->_try_insert_aux(h, kv_pair, &store_2x);
|
||||
|
||||
if (!chk.second) {
|
||||
// shenanigans - something isn't right.
|
||||
// - may have run out of memory
|
||||
assert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->store_ = std::move(store_2x);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -572,10 +637,15 @@ namespace xo {
|
|||
Hash,
|
||||
Equal>::insert(const std::pair<const Key, Value> & kv_pair)
|
||||
{
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
auto [slot_addr, ins_flag] = this->try_insert(kv_pair);
|
||||
|
||||
if (slot_addr)
|
||||
if (slot_addr) {
|
||||
log && log("fast", xtag("slot_addr", (void*)slot_addr), xtag("ins_flag", ins_flag));
|
||||
|
||||
return ins_flag;
|
||||
}
|
||||
|
||||
assert((store_.size_ + 1) / static_cast<float>(store_.n_slot_) >= c_max_load_factor);
|
||||
|
||||
|
|
@ -585,11 +655,23 @@ namespace xo {
|
|||
|
||||
return ins_flag;
|
||||
} else {
|
||||
assert(false);
|
||||
|
||||
// TODO: set last error. Presumeably reached max size
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Key,
|
||||
typename Value,
|
||||
typename Hash,
|
||||
typename Equal>
|
||||
void
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::clear()
|
||||
{
|
||||
this->store_.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify DArenaHashMap class invariants.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ namespace xo {
|
|||
|
||||
void swap(DArenaVector & other) noexcept;
|
||||
|
||||
DArenaVector & operator=(DArenaVector && x) noexcept;
|
||||
|
||||
private:
|
||||
T * _address_of(size_type i) { return ((T *)store_.lo_) + i; }
|
||||
const T * _address_of(size_type i) const { return ((const T *)store_.lo_) + i; }
|
||||
|
|
@ -126,6 +128,18 @@ namespace xo {
|
|||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
DArenaVector<T> &
|
||||
DArenaVector<T>::operator=(DArenaVector && other) noexcept
|
||||
{
|
||||
this->size_ = other.size_;
|
||||
this->store_ = std::move(other.store_);
|
||||
|
||||
other.size_ = 0;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
DArenaVector<T>
|
||||
DArenaVector<T>::map(const ArenaConfig & cfg)
|
||||
|
|
|
|||
|
|
@ -132,6 +132,48 @@ namespace xo {
|
|||
REQUIRE(n == map.size());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
map.clear();
|
||||
|
||||
REQUIRE(map.empty());
|
||||
REQUIRE(map.size() == 0);
|
||||
REQUIRE(map.groups() == 0);
|
||||
REQUIRE(map.capacity() == 0);
|
||||
}
|
||||
|
||||
/* slightly different starting point, 0 capacity! */
|
||||
{
|
||||
auto x = map.try_insert(std::make_pair(1, 11));
|
||||
|
||||
/* try_insert should fail - no capacity */
|
||||
REQUIRE(!x.first);
|
||||
REQUIRE(!x.second);
|
||||
}
|
||||
|
||||
{
|
||||
/* insert will grow hash table */
|
||||
auto x = map.insert(std::make_pair(1, 11));
|
||||
|
||||
CHECK(x);
|
||||
REQUIRE(!map.empty());
|
||||
REQUIRE(map.size() == 1);
|
||||
REQUIRE(map.groups() == 1);
|
||||
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
|
||||
REQUIRE(map.load_factor() == 1/16.0);
|
||||
|
||||
/* verify iteration */
|
||||
{
|
||||
size_t n = 0;
|
||||
for (auto & ix : map) {
|
||||
REQUIRE(ix.first == 1);
|
||||
REQUIRE(ix.second == 11);
|
||||
++n;
|
||||
}
|
||||
REQUIRE(n == map.size());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaHashMap-try-insert2", "[arena][DArenaHashMap]")
|
||||
|
|
@ -153,7 +195,7 @@ namespace xo {
|
|||
* observes test failure
|
||||
*/
|
||||
|
||||
for (std::uint32_t n = 0; n <= 2; ) {
|
||||
for (std::uint32_t n = 0; n <= 8; ) {
|
||||
HashMap hash_map;
|
||||
|
||||
auto test_fn = [&rgen, &hash_map](bool dbg_flag,
|
||||
|
|
@ -163,6 +205,9 @@ namespace xo {
|
|||
|
||||
ok_flag &= HashMapUtil<HashMap>::random_inserts(n, dbg_flag, &rgen, &hash_map);
|
||||
|
||||
ok_flag &= HashMapUtil<HashMap>::check_forward_iterator(0.0 /*dvalue*/,
|
||||
dbg_flag, hash_map);
|
||||
|
||||
return ok_flag;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <catch2/catch.hpp>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace utest {
|
||||
|
|
@ -379,6 +380,73 @@ namespace utest {
|
|||
} /*random_lookups*/
|
||||
#endif
|
||||
|
||||
/* Require:
|
||||
* - hash has keys [0..n-1] where n=map size
|
||||
* - tree value at key k is dvalue+10*k
|
||||
*/
|
||||
static bool
|
||||
check_forward_iterator(uint32_t dvalue,
|
||||
bool catch_flag,
|
||||
HashMap & map)
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::xtag;
|
||||
|
||||
/* -> flase if/when verification fails */
|
||||
bool ok_flag = true;
|
||||
|
||||
std::size_t const n = map.size();
|
||||
|
||||
scope log(XO_DEBUG(catch_flag));
|
||||
|
||||
log && log("map with size n", xtag("n", n));
|
||||
|
||||
std::unordered_set<std::size_t> keys;
|
||||
|
||||
{
|
||||
auto end_ix = map.end();
|
||||
|
||||
//log && log(xtag("end_ix", end_ix));
|
||||
|
||||
auto begin_ix = map.begin();
|
||||
auto ix = begin_ix;
|
||||
|
||||
int last_key = -1;
|
||||
|
||||
while (ix != end_ix) {
|
||||
log && log("forward loop top"
|
||||
//xtag("ix", ix)
|
||||
);
|
||||
|
||||
/* verify: keys in map are in [0 .. n) */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, 0 <= ix->first);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first < n);
|
||||
|
||||
/* verify: keys in map are unique */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, !keys.contains(ix->first));
|
||||
keys.insert(ix->first);
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10 * ix->first);
|
||||
|
||||
last_key = ix->first;
|
||||
++ix;
|
||||
|
||||
log && log("forward loop bottom",
|
||||
xtag("last_key", last_key)
|
||||
//xtag("next ix", ix)
|
||||
);
|
||||
}
|
||||
|
||||
/* should have visited exactly n locations */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == keys.size());
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix == end_ix);
|
||||
|
||||
//log && log(xtag("ix", ix), xtag("begin_ix", begin_ix));
|
||||
}
|
||||
|
||||
return ok_flag;
|
||||
}
|
||||
|
||||
#ifdef NOT_YET
|
||||
/* Require:
|
||||
* - tree has keys [0..n-1], where n=treẹsize()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue