Skip to content

perf(metrics): fast hasher for loc line sets; reserve the nesting map #1069

Description

@dekobon

Summary

Two follow-ups the #1062 hasher work identified but deliberately left out of
scope. Both are the same shape as that change: self-produced integer keys
paying for hash-flooding resistance they cannot use, plus an allocation that
does not need to happen.

1. loc's line-number sets still use SipHash

src/metrics/loc.rs:169,241,249 hold HashSet<usize> keyed by line
numbers
, constructed with HashSet::default() at :258-259 and hit on
every line inserted, unioned or extended. The argument from NodeIdHasher
(src/spaces.rs) applies verbatim — self-produced integers, never
attacker-chosen — and FxHash is a particularly good fit here: consecutive
line numbers times an odd multiplier is a bijection on the low bits, so
distribution is ideal.

Loc is default-selected and an MI input, so this is a hotter path than the
cognitive map that motivated the hasher.

2. NestingMap has no capacity reserve

src/spaces/compute.rs builds NestingMap::default() fresh per file with no
with_capacity_and_hasher. A 50 000-node file rehashes roughly 14 times
mid-walk. Either reserve from a node-count estimate, or hoist one map across
files and clear() it — clear keeps the allocation, which is the point.

This was flagged as plausibly worth more than the hash-function swap itself,
and it is cheaper to write.

Measure, do not assume

The hasher change taught this the hard way: the first numbers quoted for it
were unsupportable, and the corpus was misdescribed. Anything done here
should come with an interleaved before/after and a control metric, and
ideally wait for the harness in #1068 so the numbers are reproducible rather
than living in a commit message.


Resolution

Status: Fixed (pending merge)
Commit: 9b33344 on branch fix/issue-1069

Both sub-parts landed, both measured with an interleaved paired
protocol and controls (see the fix-summary comment for the full table):

  • loc line setsPloc/Cloc now key on IntKeyHasher
    (NodeIdHasher moved to src/int_hash.rs and renamed, since it is no
    longer node-id specific). corpus/walk/loc -7.1% (19/20 rounds,
    sign-test p < 0.0001), shape/walk/loc/nested-while -8.7% (19/20,
    p < 0.0001).
  • nesting map reserve — sized from Node::descendant_count() when
    Cognitive is selected and the grammar's impl actually seeds slots
    (new Cognitive::SEEDS_NESTING, false for the Preproc /
    Ccomment no-ops so their map stays unallocated).
    shape/walk/cognitive/nested-while -12.2% (17/20, p = 0.0026).
    Not resolvable on the corpus slice (median file ~5 KiB): two runs
    read -4.4% (p = 0.057) and +0.2% (p = 1.0). Reported as unresolved,
    not as a number.

Controls (corpus/walk/tokens, shape/walk/tokens/nested-paren,
shape/walk/nom/nested-while) sat at +0.7% to +6.6% — cross-build code
layout, which bounds the precision at roughly +/-7 points and is why
the direction and the round-by-round consistency, not the magnitude,
are the claim.

The "Measure, do not assume" requirement also produced a doc change:
the sequential --save-baseline / --baseline recipe in
docs/development/benchmarking.md reported 23% and 19% swings on
benchmarks the change cannot reach, so that doc now carries the
interleaved protocol used here.

Behaviour is bit-identical — no metric value or snapshot moved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions