Core: Share the AHashMap table implementation and add AHashSet - #10
Open
dsarno wants to merge 2 commits into
Open
Core: Share the AHashMap table implementation and add AHashSet#10dsarno wants to merge 2 commits into
dsarno wants to merge 2 commits into
Conversation
Extract the open-addressing table used by `AHashMap` into a reusable `RawAHashTable` base that is parameterized on the derived container via CRTP, so the probing, metadata and growth logic live in one place. Add `AHashSet`, an array-backed set built on the same table. It offers the same insertion-ordered, cache-friendly iteration as `AHashMap` and covers the cases where `HashSet` was used only for membership tests. Use `AHashSet` for the filter path collection in the animation blend tree editor.
Four defects in the shared table and in `AHashSet`: - `RawAHashTable` kept a virtual destructor from before the CRTP conversion. It made every `AHashMap` and `AHashSet` polymorphic and grew `sizeof(AHashMap<int, int>)` from 24 to 32 bytes. The destructor is now protected and non-virtual. - `AHashSet::insert()` overwrote an element that was already stored, while `HashSet::insert()` keeps the stored one. With a comparator that treats distinguishable elements as equal, the stored element silently changed. - `AHashSet` had no move assignment, so `set = std::move(other)` bound to copy assignment and deep-copied. Added one that swaps, matching `HashSet`. - `RawAHashTable::reset()` freed the metadata array without clearing the pointer, leaving a dangling pointer behind public API. Also covers the fixes with tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Factors the open-addressing table out of
AHashMapinto a sharedRawAHashTable, and addsAHashSeton top of it.AHashMapkeeps its behaviour and API; the storage, probing, growth and erase logic now live in one place instead of being duplicated per container.Six files: the new
raw_a_hash_table.handa_hash_set.h, the reworkeda_hash_map.{h,cpp}, one call-site update, and a new test file.Provenance
The first commit is third-party work — 36 commits squashed and rebased onto current
master, original authorship preserved (Brogolem35). The merge-base was 3,420 commits behind. The second commit is review fixes.This branch was produced by an AI agent. Everything below was built, run and measured, but it warrants a human read — this is a container used engine-wide, so the blast radius of a mistake here is large.
The tests were never being compiled
The most consequential finding.
tests/SCsubglobs*/**/*.cpp, and the new test file was namedtest_a_hash_set.h— so its 263 lines ofAHashSettests had never been built or run. Renaming it to.cppimmediately exposed a latent compile error (AHashSet<int> copy_assign = set;against anexplicitcopy constructor), fixed to match the existingHashSettest.A green CI run on the original branch would have meant nothing for this container.
Conflict resolution
a_hash_map.h— upstream had reworked the doc comment, added_WARN_UNUSED_on the class and_LIFETIME_BOUND_on ~20 accessors, and renamed iterator comparison parameters. Took upstream's annotations, kept the CRTP base andusing Base::block, and applied the same conventions to the two new files (written before that refactor landed).a_hash_map.cpp— modify/delete. The change deleted it; upstream still maintains it for explicit template instantiation of five hot types. Kept upstream's file and restored theextern templateblock, then verified the explicit instantiation still links through the CRTP base.animation_blend_tree_editor_plugin.cpp— upstream changed the key type fromStringtoNodePath. Resolved toAHashSet<NodePath>. Iteration order is unaffected: both containers are insertion-ordered and nothing erases on that path.Defects found and fixed
virtual ~RawAHashTable()left over from the pre-CRTP design made every container polymorphic —sizeof(AHashMap<int,int>)24 → 32 bytes engine-wide. Now a protected non-virtual destructor.AHashSet::insert()overwrote an already-stored element;HashSet::insert()keeps it. With a comparator that treats distinguishable elements as equal, the stored element silently changed.AHashSethad no move assignment, sos = std::move(x)bound to copy assignment and deep-copied. Added a swap-based one mirroringHashSet.reset()freed the metadata array without clearing the pointer, leaving a dangling pointer behind public API.Two of the four were found by diffing the new container's API against
HashSet's — worth completing that comparison rather than sampling it.Deliberately not fixed
Three defects were confirmed pre-existing (they reproduce identically against the pre-refactor header) and are out of scope here:
insert()argument aliases the container's own storage and the insert triggers a rehash. ASAN trace captured.reserve()can shrink capacity on an unallocated container.The first deserves its own change.
Testing
*AHashSet*17/17,*AHashMap*18/18,*HashMap*28/28,*HashSet*30/30 — 58 cases / 57,217 assertions, 0 failed.[IP] resolve_hostname, which has no IPv6 loopback in this sandbox and fails byte-identically on the master baseline binary. Net delta vs master: +18 cases, +4,824 assertions, zero new failures.std::unordered_map/std::unordered_setunder ASAN+UBSAN: 40 seeds × 4 workloads, good and adversarial hashers, allocation-owning key/value types — clean, zero leaks.AHashMap: byte-identical, before and after the fixes.Revert-proof: reverting the three testable fixes fails exactly 4 cases / 7 assertions, one per fix — including
CHECK(32 == 24)for the vtable regression andCHECK(GODOT == Godot)for the insert-overwrite. The dangling-pointer fix has no failing test and is not claimed as one; it is hardening, with a guard test added.Benchmark — the refactor is not a speedup
Standalone
g++ -O2, median of 7, N = 1,000,000, ms, lower is better (two passes, order reversed):At N = 10,000 every container is at the noise floor.
Honest reading: new vs pre-refactor
AHashMapis within ±2% on every operation. The CRTP extraction is performance-neutral, which is exactly what it should be — this is a deduplication, not an optimisation. The large wins in the table (~1.6× insert, ~1.7–2.2× lookup, ~1.5–1.8× erase, ~6× map iteration overHashMap/HashSet) predate this change. The one thing this change measurably moved was the vtable regression, now removed.Benchmark and fuzz harnesses were kept out of the commit; the working tree is clean.
What a human must decide
a_hash_map.cpp. The change deleted it, dropping explicit instantiation for five hot types; upstream's version was kept. If that deletion was deliberate, revisit.AHashMapstill has no move assignment —map = std::move(x)deep-copies. OnlyAHashSetgot one, for parity withHashSet; adding it toAHashMapchanges behaviour at every existing call site.AHashSetdeliberately omits the array API (get_by_index,get_index,erase_by_index,get_elements_ptr) thatAHashMapexposes — which is arguably the main selling point of these containers.RawAHashTable*. Worth a header comment.optimize=nonebuild — confirmed pre-existing on master, but someone should check CI's optimised build is unaffected.