perf: drop per-node hashing and allocation the searches do not need - #811
Merged
samueltardieu merged 1 commit intoSep 9, 2026
Merged
Conversation
Three places keep a hash structure whose answer is already available:
- `astar_bag` gives every node a `HashSet` of optimal parents, and collects the
goals into another. A node has as many optimal parents as it has incoming
edges, so these sets are tiny, and they are only ever cleared, appended to
and walked. Vectors do that without allocating a table per node or hashing
the indices, and the final conversion to `Vec` disappears with them.
- `dijkstra_bidirectional` keeps an `FxHashSet<N>` of settled nodes per
direction. An entry is only ever queued when it strictly improves on the
node's recorded cost, so the costs queued for a node strictly decrease and
the existing `cost > c` check already rejects every stale entry: the sets can
never reject anything the check does not. Removing them takes with them a
clone of every settled node, a hash of it, and a second hash to ask whether
the opposite side had settled it. That last question is instead answered by
the standard criterion of stopping once the two frontier costs sum to the
best path found, which needs no lookup at all. The meeting node is recorded
as a pair of indices, so rebuilding the path no longer looks it up either.
- `dijkstra_reach` hashes node indices into an `FxHashSet<usize>` to remember
what it has yielded. The indices come from `parents` and are dense, so a
`Vec<bool>` indexes straight into it.
`dfs_reach` also built a temporary vector per visited node in order to reverse
its successors; pushing them straight onto the stack and reversing that range
does the same thing without the allocation.
Paths and costs are checked against Bellman-Ford, and the reachability
iterators against it too, over several hundred random graphs.
dfs_reach, 300x300 grid: about 30% off
astar_bag, 120x120 grid: about 22% off
dijkstra_bidirectional, 500x500 grid: about 15% off
dijkstra_reach, 300x300 weighted grid: about 9% off
`dijkstra` and `astar` themselves are untouched and measure unchanged.
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.
Closes #807.
Vectors for the
astar_bagparent sets and sinks, no settled sets indijkstra_bidirectional, aVec<bool>fordijkstra_reach, and an in-place reverse fordfs_reach.Worth reviewing
dijkstra_bidirectionalis the part that changes more than a container. Dropping the settled sets also removes the question they answered — whether the popped node had been settled by the opposite search — which is how termination was decided. Termination moves to the standard criterion of stopping once the two frontier costs sum to the best complete path found. That is correct provided both directions record a meeting when they relax an edge into a node the other side has reached, which they already do, and it needs no lookup at all.The meeting node is now recorded as a pair of indices rather than a node, so rebuilding the path does not look it up either, and the per-expansion clone of the popped node goes away.
This is the assumption raised in #807.
Checking
Paths and costs verified against Bellman-Ford over 300 random graphs, with every returned path re-walked in the graph to confirm it is a real loopless walk of the announced cost. The reachability iterators are checked against the reachable set, each node yielded exactly once.
dijkstraandastarare untouched and measure unchanged, which is worth checking against, since three of the four changes are in files they share.All timings measured on Windows 11, Intel Core Ultra 7 265K, 64 GB RAM.