xo-array: + DArenaHashMap.insert

This commit is contained in:
Roland Conybeare 2026-01-07 12:27:15 -05:00
commit add201ec22
2 changed files with 242 additions and 51 deletions

View file

@ -10,6 +10,101 @@
namespace xo { namespace xo {
namespace mm { namespace mm {
struct DArenaHashMapUtil {
using size_type = std::size_t;
using control_type = std::uint8_t;
/** control: sentinel for empty slot **/
static constexpr uint8_t c_empty_slot = 0xFF;
/** control: tombstone for deleted slot **/
static constexpr uint8_t c_tombstone = 0xFE;
/** group size **/
static constexpr size_type c_group_size = 16;
/** find smallest multiple k : k * c_group_size >= n **/
static size_type lub_group_mult(size_t n) {
return (n + c_group_size - 1) / c_group_size;
}
/** find smallest x such that 2^x >= n. Return {x, 2^x} **/
static std::pair<size_type, size_type> lub_exp2(size_t n) {
size_type ngx = 0;
size_type ng = 1;
while (ng < n) {
++ngx;
ng *= 2;
}
return std::make_pair(ngx, ng);;
}
};
namespace detail {
/** @brief 16x 8-bit control bytes.
*
* Support optimization using SIMD operations
**/
struct Group {
std::array<uint8_t, DArenaHashMapUtil::c_group_size> ctrl_;
explicit Group(uint8_t * lo) {
std::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size);
}
/** find all exact matches in ctrl_[0..15] for @p h2.
* for each match set corresponding bit in return value.
* Bits {0x1, 0x2, 0x4, ...} set iff exact match on
* {ctrl_[0], ctrl_[1], ctrl_2[], ...} respectively
**/
uint16_t all_matches(uint8_t h2) const {
uint16_t retval = 0;
uint16_t bit = 1;
for (auto xi : ctrl_) {
if (xi == h2)
retval |= bit;
bit = bit << 1;
}
return retval;
}
/** find all empty sentinels in ctrl_[0..15].
* for each empty, set corresponding bit in return value.
* Bits {0x1, 0x2, 0x4, ...} set iff empty spot
* {ctrl_[0], ctrl_[1], ctrl_[2], ...} respectively
**/
uint16_t empty_matches() const {
uint16_t retval = 0;
uint16_t bit = 1;
for (auto xi : ctrl_) {
if (xi == DArenaHashMapUtil::c_empty_slot)
retval |= bit;
bit = bit << 1;
}
return retval;
}
#ifdef NOT_YET
__m128i ctrl; // 16 bytes loaded via SSE2
// Find all slots matching h2
uint16_t Match(uint8_t h2) const {
__m128i pattern = _mm_set1_epi8(h2);
__m128i result = _mm_cmpeq_epi8(ctrl, pattern);
return _mm_movemask_epi8(result); // 16-bit mask
}
// Find all empty slots (0xFF)
uint16_t MatchEmpty() const {
return _mm_movemask_epi8(_mm_cmpeq_epi8(ctrl, _mm_set1_epi8(0xFF)));
}
#endif
};
}
/** @brief flat hash map of key-value pairs using dedicated DArenas for storage /** @brief flat hash map of key-value pairs using dedicated DArenas for storage
* *
* Replicates (to the extent feasible) std::unordered_map<K,V> * Replicates (to the extent feasible) std::unordered_map<K,V>
@ -21,15 +116,16 @@ namespace xo {
typename Value, typename Value,
typename Hash = std::hash<Key>, typename Hash = std::hash<Key>,
typename Equal = std::equal_to<void>> typename Equal = std::equal_to<void>>
struct DArenaHashMap { struct DArenaHashMap : DArenaHashMapUtil {
public: public:
using size_type = std::size_t; using size_type = DArenaHashMapUtil::size_type;
using key_type = Key; using key_type = Key;
using mapped_type = Value; using mapped_type = Value;
using value_type = std::pair<const Key, Value>; using value_type = std::pair<const Key, Value>;
using key_hash = Hash; using key_hash = Hash;
using key_equal = Equal; using key_equal = Equal;
using byte = std::byte; using byte = std::byte;
using group_type = detail::Group;
/** create hash map **/ /** create hash map **/
DArenaHashMap(size_type hint_max_capacity, DArenaHashMap(size_type hint_max_capacity,
@ -39,29 +135,31 @@ namespace xo {
size_type hint_max_capacity = 0, size_type hint_max_capacity = 0,
bool debug_flag = false); bool debug_flag = false);
/** find smallest x such that 2^x >= n. Return {x, 2^x} **/
static std::pair<size_type, size_type> lub_exp2(size_t n);
static constexpr size_type group_size() { return c_group_size; }
#ifdef NOT_YET
static size_type min_groups();
static size_type min_size() { return min_groups() * c_group_size; }
#endif
size_type empty() const noexcept { return size_ == 0; } size_type empty() const noexcept { return size_ == 0; }
size_type groups() const noexcept { return n_group_; }
size_type size() const noexcept { return size_; }
size_type capacity() const noexcept { return n_group_ * c_group_size; } size_type capacity() const noexcept { return n_group_ * c_group_size; }
#ifdef NOT_YET /** insert @p kv_pair into hash map. replaces any previous value
// TODO: std::pair<iterator, bool> * stored under the same key.
void *
insert(std::pair<const Key, Value> & kv_pair) { * Return true if size incremented; false if value updated
uint64_t h = hash_(kv_pair.first); * for existing key
} **/
#endif bool insert(const std::pair<const Key, Value> & kv_pair);
private: private:
/** group size **/ /** load group abstraction from control bytes starting at @p ix **/
static constexpr std::size_t c_group_size = 16; group_type _load_group(size_type ix) const {
return group_type(&control_[ix]);
}
/** like ctrl_[ix] = h2, but maintain overflow copy
* at end of ctrl_[] array
**/
void _update_control(size_type ix, uint8_t h2);
private:
/** hash function **/ /** hash function **/
key_hash hash_; key_hash hash_;
/** key equal **/ /** key equal **/
@ -74,8 +172,11 @@ namespace xo {
* number of slots is n_group_ * c_group_size * number of slots is n_group_ * c_group_size
**/ **/
std::size_t n_group_ = 1 << n_group_exponent_; std::size_t n_group_ = 1 << n_group_exponent_;
/** control_[] partitioned into groups of c_group_size (16) consecutive elements **/ /** table has capacity for this number of {key,value} pairs **/
DArenaVector<byte> control_; std::size_t n_slot_ = n_group_ * c_group_size;
/** control_[] partitioned into groups of c_group_size (16) consecutive elements
**/
DArenaVector<uint8_t> control_;
/** slots_[] holds {key,value} pairs **/ /** slots_[] holds {key,value} pairs **/
DArenaVector<value_type> slots_; DArenaVector<value_type> slots_;
/** true to enable debug logging **/ /** true to enable debug logging **/
@ -89,6 +190,10 @@ namespace xo {
{ {
} }
/* remarks:
* - control: extra 16 slots for safe wraparound.
* last 16 bytes will be copy of first 16 bytes
*/
template <typename Key, typename Value, typename Hash, typename Equal> template <typename Key, typename Value, typename Hash, typename Equal>
DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(Hash && hash, DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(Hash && hash,
Equal && eq, Equal && eq,
@ -97,53 +202,123 @@ namespace xo {
: hash_{std::move(hash)}, : hash_{std::move(hash)},
equal_{std::move(eq)}, equal_{std::move(eq)},
size_{0}, size_{0},
n_group_exponent_{lub_exp2(hint_max_capacity).first}, n_group_exponent_{lub_exp2(lub_group_mult(hint_max_capacity)).first},
n_group_{lub_exp2(hint_max_capacity).second}, n_group_{lub_exp2(lub_group_mult(hint_max_capacity)).second},
control_{DArenaVector<byte>::map(ArenaConfig{.size_ = n_group_})}, n_slot_{n_group_ * c_group_size},
slots_{DArenaVector<value_type>::map(ArenaConfig{.size_ = n_group_ * sizeof(value_type)})}, control_{DArenaVector<uint8_t>::map(ArenaConfig{.size_ = n_slot_ + c_group_size})},
slots_{DArenaVector<value_type>::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})},
debug_flag_{debug_flag} debug_flag_{debug_flag}
{ {
} }
template <typename Key, typename Value, typename Hash, typename Equal> template <typename Key, typename Value, typename Hash, typename Equal>
auto void
DArenaHashMap<Key, Value, Hash, Equal>::lub_exp2(size_t n) -> std::pair<size_type, size_type> DArenaHashMap<Key, Value, Hash, Equal>::_update_control(size_type ix, uint8_t h2)
{ {
size_type ngx = 0; this->control_[ix] = h2;
size_type ng = 1;
while (ng < n) { if (ix < c_group_size) {
++ngx; size_type N = this->capacity();
ng *= 2;
// refresh end-of-array copy
std::memcpy(&(control_[N]), &(control_[0]), c_group_size);
} }
return std::make_pair(ngx, ng);;
} }
#ifdef NOT_YET
template <typename Key, typename Value, typename Hash, typename Equal> template <typename Key, typename Value, typename Hash, typename Equal>
auto bool
DArenaHashMap<Key, Value, Hash, Equal>::min_groups() -> size_type DArenaHashMap<Key, Value, Hash, Equal>::insert(const std::pair<const Key, Value> & kv_pair)
{ {
size_type page_z = getpagesize(); size_type h = hash_(kv_pair.first);
// h1: hi bits: probe sequence
size_type h1 = h >> 7;
// h2: lo bits: store in control byte
uint8_t h2 = h & 0x7f;
// 1 page of slots size_type N = this->capacity();
size_type n_slot = page_z / sizeof(value_type);
// 1 page of groups // same as:
size_type n_group = n_slot / c_group_size; // ix = h1 % N
// since N is power of 2
size_type ix = h1 & (N - 1);
// glb power of 2, but at least 1 // will make series of probes
size_type ng = 1; for (;;) {
auto grp = _load_group(ix);
while (2 * ng < n_group) {
ng *= 2; // look for matching slot to update
uint16_t m = grp.all_matches(h2);
return ng; // process each match.
// matches are encountered in the same order they
// appear in ctrl_[]
while (m) {
// zeroes: #of 0 before least-significant 1 bit
int skip = __builtin_ctz(m);
size_type slot_ix = (ix + skip) & (N - 1);
// invariant: slot_ix in [0 .. N)
auto & slot = slots_[slot_ix];
if (slot.first == kv_pair.first) {
// we have match on existing key;
// replace associated value
slot.second = kv_pair.second;
// false: did not change table size
return false;
}
// e.g:
// /-- lowest 1 bit gets cleared
// v
// m = b01101000
// m-1 = b01100111
// & = b01100000
m &= (m - 1);
}
}
{
// look for empty slot to insert
uint16_t e = grp.empty_matches();
// process each empty slot
if (e) {
// zeroes: #of 0 before least significant 1 bit
int skip = __builtin_ctz(e);
size_type slot_ix = (ix + skip) & (N - 1);
// invariant: slot_ix in [0 .. N)
auto & slot = slots_[slot_ix];
// mark slot occupied in control space;
// maintain copy-at-end for overflow
this->update_control(slot_ix, h2);
new (&slot) value_type(kv_pair);
++(this->size_);
// true: increased table size
return true;
}
}
// slot range associated with grp
// has no room, and does not contain target key
// -> move on to next group.
//
// note: relying on c_group_size overflow bytes here
// when ix is close to N
ix = (ix + c_group_size) & (N - 1);
}
} }
#endif
} }
} /*namespace xo*/ } /*namespace xo*/

View file

@ -7,6 +7,7 @@
#include <catch2/catch.hpp> #include <catch2/catch.hpp>
namespace xo { namespace xo {
using xo::mm::DArenaHashMapUtil;
using xo::mm::DArenaHashMap; using xo::mm::DArenaHashMap;
//using xo::mM::ArenaConfig; //using xo::mM::ArenaConfig;
@ -18,7 +19,22 @@ namespace xo {
HashMap map; HashMap map;
REQUIRE(map.empty()); REQUIRE(map.empty());
REQUIRE(map.capacity() == HashMap::group_size()); REQUIRE(map.size() == 0);
REQUIRE(map.groups() == 1);
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
}
TEST_CASE("DArenaHashMap-ctor2", "[arena][DArenaHashMap]")
{
using HashMap = DArenaHashMap<int, int>;
HashMap map(257);
REQUIRE(map.empty());
REQUIRE(map.size() == 0);
REQUIRE(map.capacity() == map.groups() * DArenaHashMapUtil::c_group_size);
REQUIRE(map.capacity() == std::max(512ul,
DArenaHashMapUtil::c_group_size));
} }
} }
} }