Skip to content

Binary search in PiecewiseTrajectory::nearestIndex - #227

Merged
brownd1978 merged 1 commit into
KFTrack:mainfrom
RobMina:nearestindex-bsearch
Oct 17, 2026
Merged

brownd1978 merged 1 commit into
KFTrack:mainfrom
RobMina:nearestindex-bsearch

Conversation

@RobMina

@RobMina RobMina commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

PiecewiseTrajectory::nearestIndex found the piece holding a time by scanning the piece deque from the front. It is
called for every closest-approach update of a hit or material crossing, every append, nearestPiece and
DomainWall::updateReference, so in Mu2e reconstruction the scan is one of the two most expensive functions wherever
a KinKal fit runs (callgrind, Offline main + KinKal 3.8.0):

Workload (Mu2e reco) nearestIndex share of all event-processing instructions
on-spill CE + 1BB pileup 10.4 % (91,500 calls/event)
off-spill cosmics 16.1 %
extracted-detector cosmics (KinematicLine, no B-field domains) 3.6 %

Its cost grows with the number of pieces, i.e. with the number of B-field domains, so long tracks pay most.

The pieces are time-ordered and contiguous (append/prepend/add keep begin(i+1) == end(i)), so this PR replaces
the scan with std::lower_bound over the piece end times: O(log N) instead of O(N). The clamps for times before the
first and after the last piece are unchanged.

Why the result is identical

Inside the range, the loop stopped at the first piece i with inRange(t) || t <= end(i). Since inRange(t) implies
t < end(i), that is the first piece with end(i) >= t, which is what lower_bound with the predicate
end(i) < t returns, given non-decreasing end times. A time on a boundary, t == end(i) == begin(i+1), returns i
in both; zero-length pieces and NaN also give the same index. pieces_ is a std::deque, so the iterators are random
access.

Tests

  • Standalone, header-only: the new function against a verbatim copy of the loop, on 2000 random trajectories of 1 to
    10,000 pieces (every piece's begin, end, end minus one ulp and midpoint, random times in and around the range, NaN,
    zero-length interior pieces): 9,346,772 queries, 0 mismatches. Per call: 0.9x at 10 pieces, 2.5x faster at 100,
    17x at 1,000, 108x at 10,000.
  • KinKal unit tests: 26/26 pass.
  • Mu2e reconstruction, output: bitwise identical (every numeric EventNtuple leaf, relative tolerance 0) and equal
    TrigReports on
    • on-spill CE + 1BB MC, LoopHelix fits (4 hypotheses), 2000 events: 479 leaves;
    • off-spill cosmic MC, CentralHelix fit with extrapolation to the CRV, 10,000 events: 530 leaves.
  • Mu2e reconstruction, typical-event cost (same host, same events, base = unpatched build of the same commits):
Sample Quantity Before After Change
on-spill CE + 1BB, 2000 events job CPU 266.5 s 252.9 s -5.1 %
median event 125.8 ms 119.8 ms -4.8 %
each LoopHelix fit module (mean) 16.4-17.9 ms 14.9-16.2 ms -9 to -11 %
off-spill cosmics, 2000 events (one pathological event excluded) mean event 158.5 ms 133.4 ms -15.8 %
median event 95.6 ms 86.3 ms -9.8 %
LoopHelix fit modules (mean) 24.7-30.6 ms 22.0-26.7 ms -11 to -13 %
CentralHelix fit module (mean) 10.0 ms 8.7 ms -13 %

Modules that do not use KinKal are unchanged within run-to-run noise. The excluded off-spill event is a pathological
seed with ~157k B-field domains, handled separately in Mu2e/Offline#2044; this change also makes it about 50 times
cheaper, but the numbers above do not depend on it.

Not changed

pieceRange (both overloads) and setRange(trim=true) still walk the deque linearly; they do not show up in the
profiles. They could reuse the same search in a follow-up.

@brownd1978 brownd1978 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Rob for this useful improvement!

@brownd1978
brownd1978 merged commit 14fd223 into KFTrack:main Oct 17, 2026
4 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.

2 participants