Skip to content

HermitCrab FST: use flat register arrays in the traversal (CreateInstanceMDArray 11.5% to 0.2% of profile, wall-clock only) #591

Open

Description

@johnml1135

The FST traversal in SIL.Machine.FiniteState allocates Register<TOffset>[,], a multi-dimensional array, once per match call and per traversal instance. This goes through the slow Array.CreateInstanceMDArray allocator. The change uses a flat Register<TOffset>[] of length registerCount * 2, with an index helper. Internally it uses Array.Copy and a flat-layout RegistersEqualityComparer. The public FstResult.Registers stays two-dimensional and is built once per accepting state. It is a .NET allocation fix and does not port to Rust. The change is on the archived branch perf/hc-optimization-archive (the helper RegisterArray.Idx is not on master).

Measured. On the heaviest Sena word, CreateInstanceMDArray fell from 11.5% to 0.2% of the CPU profile (8.6% of it was under Matcher.AllMatches, 2.3% in traversal setup). Wall-clock only, not attributed: it was not timed alone. Counts are unchanged by construction.

What a revival must do first. Time it alone, since the 11.5% is a profile share and not a measured saving. Keep the RegistersEqualityComparerTests equality and hashing coverage.

Record: see the archived write-up below
Ledger: #490 (comment); disposition: #490 (comment)
From #490 (closed).

Activity

  1. johnml1135 commented on Nov 2, 2026

    @johnml1135
    CollaboratorAuthor

    Claude-drafted.

    Related and unmeasured: the same traversal hashes each new visited-set key twice, in Contains then Add, and allocates a Tuple per candidate. No profile on disk gives its share. This is not the SipHash cost PanGloss removed in sillsdev/PanGloss#123; C# has none.

    Next: on the heaviest Sena word, read the inclusive time of KeyGetHashCode, RegistersEqualityComparer.GetHashCode and HashSet.Contains/Add when this issue is timed. Under about 3%, drop it. At 5% or more, file an issue for a single Add.

  2. johnml1135 commented on Nov 7, 2026

    @johnml1135
    CollaboratorAuthor

    Archived from docs/hc-optimization-ledger @ 2eba2f5 (docs/optimizations/flat-register-arrays.md), which is being deleted:

    Flat register arrays in the FST traversal

    Status: retained on perf/hc-optimization-archive, not merged (SIL.Machine.FiniteState:
    Register<TOffset>[] indexed by RegisterArray.Idx(r, k) replaces Register<TOffset>[,] internally; the
    public FstResult.Registers stays two-dimensional and is materialized once per accepting state).
    Portable: no; a .NET-specific allocation path.

    The observation

    11.5% of the heaviest Sena word's CPU profile was inside System.Array.CreateInstanceMDArray, the slow
    generic allocator for multi-dimensional arrays, called from Matcher.AllMatches (8.6%) and traversal
    setup (2.3%): one new Register<TOffset>[n, 2] per match call and per traversal instance.

    The change

    Mechanical: flat arrays of length registerCount * 2, a two-constant index helper, Array.Copy on the
    flat struct array, and RegistersEqualityComparer comparing the flat layout. No behaviour change.

    Measured

    CreateInstanceMDArray 11.5% → 0.2% of the profile. Counts unchanged by construction. Together with the
    other allocation changes it contributed to the heaviest word's wall time; not measured in isolation.

    RegistersEqualityComparerTests covers equality and hashing of the flattened layout, while the existing FSA
    and FST suites cover accepting results and register materialization. The complete conformance-inclusive run
    passed 603 tests with one expected host-privilege skip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions