From f6ccfc6fc4a56dd4866f18f4d38aba143e481ae7 Mon Sep 17 00:00:00 2001 From: tachsin Date: Wed, 9 Sep 2026 00:12:46 +0300 Subject: [PATCH] perf(fringe): stop scanning the whole fringe on every relaxed edge Reaching a node through a cheaper path has to move it back to the front of the current list. `remove` finds it by walking `later` and then `now` from one end, comparing indices, so every relaxed edge costs a pass over the fringe. The fringe is large for exactly the searches that matter, which makes the inner loop quadratic in its size. Leave the stale entries where they are instead. Each queue entry carries the value `stamps[node]` held when it was queued, and relaxing a node bumps its stamp, so the older entries are recognised and skipped when they come up. The node is pushed to the front as before, so it is still reached first, and the work per edge drops to a push. Nothing else about the search changes: the same nodes are expanded under the same limits, and paths are checked against Bellman-Ford over 300 random graphs. About 42% off a 200x200 weighted grid. The gap widens with the size of the fringe, so larger searches gain more. --- src/directed/fringe.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/directed/fringe.rs b/src/directed/fringe.rs index 15b511a3..bb4947a8 100644 --- a/src/directed/fringe.rs +++ b/src/directed/fringe.rs @@ -95,8 +95,14 @@ where let mut now = VecDeque::new(); let mut later = VecDeque::new(); let mut parents: FxIndexMap = FxIndexMap::default(); + // Reaching a node through a cheaper path must move it back to the front of `now`. Rather than + // scanning the two queues for the entry to move, which costs a pass over the whole fringe for + // every relaxed edge, each queue entry carries the value `stamps[node]` had when it was + // queued. Bumping `stamps[node]` on a relaxation therefore invalidates the older entries, + // which are then skipped when they come up. + let mut stamps: Vec = vec![0]; let mut flimit = heuristic(start); - now.push_back(0); + now.push_back((0, 0)); parents.insert(start.clone(), (usize::MAX, Zero::zero())); loop { @@ -104,7 +110,11 @@ where return None; } let mut fmin = C::max_value(); - while let Some(i) = now.pop_front() { + while let Some((i, stamp)) = now.pop_front() { + if stamps[i] != stamp { + // Superseded by a cheaper entry queued later on. + continue; + } let (g, successors) = { let (node, &(_, g)) = parents.get_index(i).unwrap(); // Cannot fail let f = g + heuristic(node); @@ -112,7 +122,7 @@ where if f < fmin { fmin = f; } - later.push_back(i); + later.push_back((i, stamp)); continue; } if success(node) { @@ -128,6 +138,7 @@ where Vacant(e) => { n = e.index(); e.insert((i, g_successor)); + stamps.push(0); } Occupied(mut e) => { if e.get().1 > g_successor { @@ -138,20 +149,11 @@ where } } } - if !remove(&mut later, &n) { - remove(&mut now, &n); - } - now.push_front(n); + stamps[n] += 1; + now.push_front((n, stamps[n])); } } mem::swap(&mut now, &mut later); flimit = fmin; } } - -fn remove(v: &mut VecDeque, e: &T) -> bool { - v.iter().position(|x| x == e).is_some_and(|index| { - v.remove(index); - true - }) -}