perf(timer): inline TimerTable::pop_expired_nonempty

Drop the #[cold] attribute and add #[inline]. The hint-cold was
keeping the body out-of-line at every call site (one per chart that
declares any after() trigger), which forced a real function call
through the x86_64 ABI plus its register-spill prologue/epilogue —
~10-15 instructions of pure framing on top of the body's own cost.
Inlining lets LLVM fold the body into the codegen-emitted
timer_pop_block at step()'s timer-pop site; for the timer_fire
bench, run_handlers also gets inlined into step() as a knock-on,
because the inliner's budget is no longer being spent on a separate
pop_expired_nonempty call.

Bench delta (vs. after_opt2_unchecked, instructions):

  timer_fire_dispatch    212 → 175   -17%
  bubble_top              94 →  93    -1%   (incidental)
  others: unchanged

Code-size cost: pop_expired_nonempty is short and inlines cheaply;
the binary size before/after differs by a handful of bytes per
chart-with-timers call site. The original #[cold] attribute appears
to have been a guess (timer arming is rare → mark cold) that was
wrong: when a chart has timers at all, the empty-check in pop_expired
is the cold path; pop_expired_nonempty itself runs on every step()
of any state with an active timer.
This commit is contained in:
swaits committed 2026-05-04 15:29:23 -06:00
1 parent d5ef6a6944
commit 8cd1aea646
1 file changed
+1 -1
+1 -1
View File
@@ -895,7 +895,7 @@ pub mod __private {
}
}
#[cold]
#[inline]
fn pop_expired_nonempty(&mut self, depth: &[u8]) -> Option<(u16, u16)> {
let mut best: Option<usize> = None;
for (i, e) in self.entries.iter().enumerate() {