#pragma once /* HashMap that is optimized for embedded devices. The hashmap will store upto N elements inline, and will spill over to a heap. This hashmap will try not to grow by detecting during rehash that the number of tombstones is greater than the number of elements. This will keep the memory from growing during multiple inserts and removals. */ // #include // #include #include "fl/assert.h" #include "fl/bitset.h" #include "fl/clamp.h" #include "fl/hash.h" #include "fl/map_range.h" #include "fl/optional.h" #include "fl/pair.h" #include "fl/type_traits.h" #include "fl/vector.h" #include "fl/warn.h" #include "fl/align.h" #include "fl/compiler_control.h" #include "fl/math_macros.h" #include "fl/compiler_control.h" FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_SHORTEN_64_TO_32 namespace fl { // // begin using declarations for stl compatibility // use fl::equal_to; // use fl::hash_map; // use fl::unordered_map; // // end using declarations for stl compatibility FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_NULL_DEREFERENCE template struct EqualTo { bool operator()(const T &a, const T &b) const { return a == b; } }; FL_DISABLE_WARNING_POP // -- HashMap class // ------------------------------------------------------------- Begin HashMap // class #ifndef FASTLED_HASHMAP_INLINED_COUNT #define FASTLED_HASHMAP_INLINED_COUNT 8 #endif template , typename KeyEqual = EqualTo, int INLINED_COUNT = FASTLED_HASHMAP_INLINED_COUNT> class FL_ALIGN HashMap { public: HashMap() : HashMap(FASTLED_HASHMAP_INLINED_COUNT, 0.7f) {} HashMap(fl::size initial_capacity) : HashMap(initial_capacity, 0.7f) {} HashMap(fl::size initial_capacity, float max_load) : _buckets(next_power_of_two(initial_capacity)), _size(0), _tombstones(0), _occupied(next_power_of_two(initial_capacity)), _deleted(next_power_of_two(initial_capacity)) { setLoadFactor(max_load); } void setLoadFactor(float f) { f = fl::clamp(f, 0.f, 1.f); mLoadFactor = fl::map_range(f, 0.f, 1.f, 0, 255); } // Iterator support. struct iterator { // Standard iterator typedefs // using difference_type = std::ptrdiff_t; using value_type = pair; // Keep const Key as per standard using pointer = value_type *; using reference = value_type &; // using iterator_category = std::forward_iterator_tag; iterator() : _map(nullptr), _idx(0) {} iterator(HashMap *m, fl::size idx) : _map(m), _idx(idx) { advance_to_occupied(); } value_type operator*() const { auto &e = _map->_buckets[_idx]; return {e.key, e.value}; } pointer operator->() { // Use reinterpret_cast since pair and pair are different types // but have the same memory layout, then destroy/reconstruct to avoid assignment issues using mutable_value_type = pair; auto& mutable_cached = *reinterpret_cast(&_cached_value); mutable_cached.~mutable_value_type(); new (&mutable_cached) mutable_value_type(operator*()); return &_cached_value; } pointer operator->() const { // Use reinterpret_cast since pair and pair are different types // but have the same memory layout, then destroy/reconstruct to avoid assignment issues using mutable_value_type = pair; auto& mutable_cached = *fl::bit_cast(&_cached_value); mutable_cached.~mutable_value_type(); new (&mutable_cached) mutable_value_type(operator*()); return &_cached_value; } iterator &operator++() { ++_idx; advance_to_occupied(); return *this; } iterator operator++(int) { iterator tmp = *this; ++(*this); return tmp; } bool operator==(const iterator &o) const { return _map == o._map && _idx == o._idx; } bool operator!=(const iterator &o) const { return !(*this == o); } void advance_to_occupied() { if (!_map) return; fl::size cap = _map->_buckets.size(); while (_idx < cap && !_map->is_occupied(_idx)) ++_idx; } private: HashMap *_map; fl::size _idx; mutable value_type _cached_value; friend class HashMap; }; struct const_iterator { // Standard iterator typedefs // using difference_type = std::ptrdiff_t; using value_type = pair; using pointer = const value_type *; using reference = const value_type &; // using iterator_category = std::forward_iterator_tag; const_iterator() : _map(nullptr), _idx(0) {} const_iterator(const HashMap *m, fl::size idx) : _map(m), _idx(idx) { advance_to_occupied(); } const_iterator(const iterator &it) : _map(it._map), _idx(it._idx) {} value_type operator*() const { auto &e = _map->_buckets[_idx]; return {e.key, e.value}; } pointer operator->() const { // Use reinterpret_cast since pair and pair are different types // but have the same memory layout, then destroy/reconstruct to avoid assignment issues using mutable_value_type = pair; auto& mutable_cached = *reinterpret_cast(&_cached_value); mutable_cached.~mutable_value_type(); new (&mutable_cached) mutable_value_type(operator*()); return &_cached_value; } const_iterator &operator++() { ++_idx; advance_to_occupied(); return *this; } const_iterator operator++(int) { const_iterator tmp = *this; ++(*this); return tmp; } bool operator==(const const_iterator &o) const { return _map == o._map && _idx == o._idx; } bool operator!=(const const_iterator &o) const { return !(*this == o); } void advance_to_occupied() { if (!_map) return; fl::size cap = _map->_buckets.size(); while (_idx < cap && !_map->is_occupied(_idx)) ++_idx; } friend class HashMap; private: const HashMap *_map; fl::size _idx; mutable value_type _cached_value; }; iterator begin() { return iterator(this, 0); } iterator end() { return iterator(this, _buckets.size()); } const_iterator begin() const { return const_iterator(this, 0); } const_iterator end() const { return const_iterator(this, _buckets.size()); } const_iterator cbegin() const { return const_iterator(this, 0); } const_iterator cend() const { return const_iterator(this, _buckets.size()); } static bool NeedsRehash(fl::size size, fl::size bucket_size, fl::size tombstones, u8 load_factor) { // (size + tombstones) << 8 : multiply numerator by 256 // capacity * max_load : denominator * threshold u32 lhs = (size + tombstones) << 8; u32 rhs = (bucket_size * load_factor); return lhs > rhs; } // returns true if (size + tombs)/capacity > _max_load/256 bool needs_rehash() const { return NeedsRehash(_size, _buckets.size(), _tombstones, mLoadFactor); } // insert or overwrite void insert(const Key &key, const T &value) { const bool will_rehash = needs_rehash(); if (will_rehash) { // if half the buckets are tombstones, rehash inline to prevent // memory spill over into the heap. if (_tombstones > _size) { rehash_inline_no_resize(); } else { rehash(_buckets.size() * 2); } } fl::size idx; bool is_new; fl::pair p = find_slot(key); idx = p.first; is_new = p.second; if (is_new) { _buckets[idx].key = key; _buckets[idx].value = value; mark_occupied(idx); ++_size; } else { FASTLED_ASSERT(idx != npos(), "HashMap::insert: invalid index at " << idx << " which is " << npos()); _buckets[idx].value = value; } } // Move version of insert void insert(Key &&key, T &&value) { const bool will_rehash = needs_rehash(); if (will_rehash) { // if half the buckets are tombstones, rehash inline to prevent // memory spill over into the heap. if (_tombstones > _size) { rehash_inline_no_resize(); } else { rehash(_buckets.size() * 2); } } fl::size idx; bool is_new; fl::pair p = find_slot(key); idx = p.first; is_new = p.second; if (is_new) { _buckets[idx].key = fl::move(key); _buckets[idx].value = fl::move(value); mark_occupied(idx); ++_size; } else { FASTLED_ASSERT(idx != npos(), "HashMap::insert: invalid index at " << idx << " which is " << npos()); _buckets[idx].value = fl::move(value); } } // remove key; returns true if removed bool remove(const Key &key) { auto idx = find_index(key); if (idx == npos()) return false; mark_deleted(idx); --_size; ++_tombstones; return true; } bool erase(const Key &key) { return remove(key); } // Iterator-based erase - more efficient when you already have the iterator position iterator erase(iterator it) { if (it == end() || it._map != this) { return end(); // Invalid iterator } // Mark the current position as deleted mark_deleted(it._idx); --_size; ++_tombstones; // Advance to next valid element and return iterator to it ++it._idx; it.advance_to_occupied(); return it; } void clear() { _buckets.assign(_buckets.size(), Entry{}); _occupied.reset(); _deleted.reset(); _size = _tombstones = 0; } // find pointer to value or nullptr T *find_value(const Key &key) { auto idx = find_index(key); return idx == npos() ? nullptr : &_buckets[idx].value; } const T *find_value(const Key &key) const { auto idx = find_index(key); return idx == npos() ? nullptr : &_buckets[idx].value; } iterator find(const Key &key) { auto idx = find_index(key); return idx == npos() ? end() : iterator(this, idx); } const_iterator find(const Key &key) const { auto idx = find_index(key); return idx == npos() ? end() : const_iterator(this, idx); } bool contains(const Key &key) const { auto idx = find_index(key); return idx != npos(); } // access or default-construct T &operator[](const Key &key) { fl::size idx; bool is_new; fl::pair p = find_slot(key); idx = p.first; is_new = p.second; // Check if find_slot failed to find a valid slot (HashMap is full) if (idx == npos()) { // Need to resize to make room if (needs_rehash()) { // if half the buckets are tombstones, rehash inline to prevent // memory growth. Otherwise, double the size. if (_tombstones >= _buckets.size() / 2) { rehash_inline_no_resize(); } else { rehash(_buckets.size() * 2); } } else { // Force a rehash with double size if needs_rehash() doesn't detect the issue rehash(_buckets.size() * 2); } // Try find_slot again after resize p = find_slot(key); idx = p.first; is_new = p.second; // If still npos() after resize, something is seriously wrong if (idx == npos()) { // This should never happen after a successful resize static T default_value{}; return default_value; } } if (is_new) { _buckets[idx].key = key; _buckets[idx].value = T{}; mark_occupied(idx); ++_size; } return _buckets[idx].value; } fl::size size() const { return _size; } bool empty() const { return _size == 0; } fl::size capacity() const { return _buckets.size(); } private: static fl::size npos() { return static_cast(-1); } // Helper methods to check entry state bool is_occupied(fl::size idx) const { return _occupied.test(idx); } bool is_deleted(fl::size idx) const { return _deleted.test(idx); } bool is_empty(fl::size idx) const { return !is_occupied(idx) && !is_deleted(idx); } void mark_occupied(fl::size idx) { _occupied.set(idx); _deleted.reset(idx); } void mark_deleted(fl::size idx) { _occupied.reset(idx); _deleted.set(idx); } void mark_empty(fl::size idx) { _occupied.reset(idx); _deleted.reset(idx); } struct alignas(fl::max_align::value) Entry { Key key; T value; void swap(Entry &other) { fl::swap(key, other.key); fl::swap(value, other.value); } }; static fl::size next_power_of_two(fl::size n) { fl::size p = 1; while (p < n) p <<= 1; return p; } pair find_slot(const Key &key) const { const fl::size cap = _buckets.size(); const fl::size mask = cap - 1; const fl::size h = _hash(key) & mask; fl::size first_tomb = npos(); if (cap <= 8) { // linear probing for (fl::size i = 0; i < cap; ++i) { const fl::size idx = (h + i) & mask; if (is_empty(idx)) return {first_tomb != npos() ? first_tomb : idx, true}; if (is_deleted(idx)) { if (first_tomb == npos()) first_tomb = idx; } else if (is_occupied(idx) && _equal(_buckets[idx].key, key)) { return {idx, false}; } } } else { // quadratic probing up to 8 tries fl::size i = 0; for (; i < 8; ++i) { const fl::size idx = (h + i + i * i) & mask; if (is_empty(idx)) return {first_tomb != npos() ? first_tomb : idx, true}; if (is_deleted(idx)) { if (first_tomb == npos()) first_tomb = idx; } else if (is_occupied(idx) && _equal(_buckets[idx].key, key)) { return {idx, false}; } } // fallback to linear for the rest for (; i < cap; ++i) { const fl::size idx = (h + i) & mask; if (is_empty(idx)) return {first_tomb != npos() ? first_tomb : idx, true}; if (is_deleted(idx)) { if (first_tomb == npos()) first_tomb = idx; } else if (is_occupied(idx) && _equal(_buckets[idx].key, key)) { return {idx, false}; } } } return {npos(), false}; } enum { kLinearProbingOnlySize = 8, kQuadraticProbingTries = 8, }; fl::size find_index(const Key &key) const { const fl::size cap = _buckets.size(); const fl::size mask = cap - 1; const fl::size h = _hash(key) & mask; if (cap <= kLinearProbingOnlySize) { // linear probing for (fl::size i = 0; i < cap; ++i) { const fl::size idx = (h + i) & mask; if (is_empty(idx)) return npos(); if (is_occupied(idx) && _equal(_buckets[idx].key, key)) return idx; } } else { // quadratic probing up to 8 tries fl::size i = 0; for (; i < kQuadraticProbingTries; ++i) { const fl::size idx = (h + i + i * i) & mask; if (is_empty(idx)) return npos(); if (is_occupied(idx) && _equal(_buckets[idx].key, key)) return idx; } // fallback to linear for the rest for (; i < cap; ++i) { const fl::size idx = (h + i) & mask; if (is_empty(idx)) return npos(); if (is_occupied(idx) && _equal(_buckets[idx].key, key)) return idx; } } return npos(); } fl::size find_unoccupied_index_using_bitset( const Key &key, const fl::bitset<1024> &occupied_set) const { const fl::size cap = _buckets.size(); const fl::size mask = cap - 1; const fl::size h = _hash(key) & mask; if (cap <= kLinearProbingOnlySize) { // linear probing for (fl::size i = 0; i < cap; ++i) { const fl::size idx = (h + i) & mask; bool occupied = occupied_set.test(idx); if (occupied) { continue; } return idx; } } else { // quadratic probing up to 8 tries fl::size i = 0; for (; i < kQuadraticProbingTries; ++i) { const fl::size idx = (h + i + i * i) & mask; bool occupied = occupied_set.test(idx); if (occupied) { continue; } return idx; } // fallback to linear for the rest for (; i < cap; ++i) { const fl::size idx = (h + i) & mask; bool occupied = occupied_set.test(idx); if (occupied) { continue; } return idx; } } return npos(); } void rehash(fl::size new_cap) { new_cap = next_power_of_two(new_cap); fl::vector_inlined old; fl::bitset<1024> old_occupied = _occupied; _buckets.swap(old); _buckets.clear(); _buckets.assign(new_cap, Entry{}); _occupied.reset(); _occupied.resize(new_cap); _deleted.reset(); _deleted.resize(new_cap); _size = _tombstones = 0; for (fl::size i = 0; i < old.size(); i++) { if (old_occupied.test(i)) insert(fl::move(old[i].key), fl::move(old[i].value)); } } // Rehash the inline buckets without resizing FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_NULL_DEREFERENCE void rehash_inline_no_resize() { // filter out tombstones and compact fl::size cap = _buckets.size(); fl::size pos = 0; // compact live elements to the left. for (fl::size i = 0; i < cap; ++i) { if (is_occupied(i)) { if (pos != i) { _buckets[pos] = _buckets[i]; mark_empty(i); mark_occupied(pos); } ++pos; } else if (is_deleted(i)) { mark_empty(i); } } fl::bitset<1024> occupied; // Preallocate a bitset of size 1024 // swap the components, this will happen at most N times, // use the occupied bitset to track which entries are occupied // in the array rather than just copied in. fl::optional tmp; for (fl::size i = 0; i < _size; ++i) { const bool already_finished = occupied.test(i); if (already_finished) { continue; } auto &e = _buckets[i]; // FASTLED_ASSERT(e.state == EntryState::Occupied, // "HashMap::rehash_inline_no_resize: invalid // state"); fl::size idx = find_unoccupied_index_using_bitset(e.key, occupied); if (idx == npos()) { // no more space FASTLED_ASSERT( false, "HashMap::rehash_inline_no_resize: invalid index at " << idx << " which is " << npos()); return; } // if idx < pos then we are moving the entry to a new location FASTLED_ASSERT(!tmp, "HashMap::rehash_inline_no_resize: invalid tmp"); if (idx >= _size) { // directly copy it now _buckets[idx] = e; continue; } tmp = e; occupied.set(idx); _buckets[idx] = *tmp.ptr(); while (!tmp.empty()) { // we have to find a place for temp. // find new position for tmp. auto key = tmp.ptr()->key; fl::size new_idx = find_unoccupied_index_using_bitset(key, occupied); if (new_idx == npos()) { // no more space FASTLED_ASSERT( false, "HashMap::rehash_inline_no_resize: invalid index at " << new_idx << " which is " << npos()); return; } occupied.set(new_idx); if (new_idx < _size) { // we have to swap the entry at new_idx with tmp fl::optional tmp2 = _buckets[new_idx]; _buckets[new_idx] = *tmp.ptr(); tmp = tmp2; } else { // we can just move tmp to new_idx _buckets[new_idx] = *tmp.ptr(); tmp.reset(); } } FASTLED_ASSERT( occupied.test(i), "HashMap::rehash_inline_no_resize: invalid occupied at " << i); FASTLED_ASSERT( tmp.empty(), "HashMap::rehash_inline_no_resize: invalid tmp at " << i); } // Reset tombstones count since we've cleared all deleted entries _tombstones = 0; } FL_DISABLE_WARNING_POP fl::vector_inlined _buckets; fl::size _size; fl::size _tombstones; u8 mLoadFactor; fl::bitset<1024> _occupied; fl::bitset<1024> _deleted; Hash _hash; KeyEqual _equal; }; // begin using declarations for stl compatibility template using equal_to = EqualTo; template , typename KeyEqual = equal_to> using hash_map = HashMap; template , typename KeyEqual = equal_to> using unordered_map = HashMap; // end using declarations for stl compatibility } // namespace fl FL_DISABLE_WARNING_POP