Skip to content

perf: drop per-node hashing and allocation the searches do not need - #811

Merged
samueltardieu merged 1 commit into
evenfurther:mainfrom
tachsin:perf/search-per-node-allocations
Sep 9, 2026
Merged

perf: drop per-node hashing and allocation the searches do not need#811
samueltardieu merged 1 commit into
evenfurther:mainfrom
tachsin:perf/search-per-node-allocations

Conversation

@tachsin

@tachsin tachsin commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Closes #807.

Vectors for the astar_bag parent sets and sinks, no settled sets in dijkstra_bidirectional, a Vec<bool> for dijkstra_reach, and an in-place reverse for dfs_reach.

Worth reviewing

dijkstra_bidirectional is 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.

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 are 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.

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.
@samueltardieu
samueltardieu added this pull request to the merge queue Sep 9, 2026
Merged via the queue into evenfurther:main with commit 1af002b Sep 9, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Several searches keep per-node hash structures whose answers are already available

2 participants