vt: decode CSI parameter runs in a register #425

Merged
natsukium merged 3 commits from vt/faster-param-runs into main 2026-09-25 10:20:07 +09:00
Owner

Closes #417.

Params::push_run fed every byte through push_digit / next_slot, so each digit reloaded len, re-checked it, and did a saturating multiply on the slot in memory, reading back the previous digit's store. It now keeps the open slot's value and the slot count in registers and writes the slot once per separator and once at the end of the run (min(10v + d, u16::MAX) in u32 is the same fold as the per-byte saturating ops). The bulk arm in advance also takes the first parameter byte in CsiEntry (digit or ;; a leading : stays per-byte because start_empty opens no subparam slot), saving one advance_byte round trip per CSI.

Measurements

Floor bench measure_parse_into_grid_floor, 35x106 (DOOM-fire capture geometry), median of 5 interleaved runs, load ~3, instructions:u over the bench's two passes:

payload MiB/s base → new instructions
doom (56 MiB DOOM-fire capture) 218 → 241 (+10.8%) 10350M → 9409M (−9.1%)
truecolor (per-cell 38;2/48;2) 297 → 345 (+16.1%) 5257M → 4476M (−14.9%)
csi (kitten csi mix) 143 → 154 (+7.3%) 9315M → 8833M (−5.2%)
sparse scroll (y\r\n) 96.3 → 96.9 (+0.6%) 30542M → 30961M (+1.4%)

The sparse scroll floor is flat over 10 interleaved runs (96.9 vs 96.5 MiB/s median, cycles within 0.5%), so the accepted cost in cost-maps does not grow.

Rejected (numbers in the commit body and cost-maps): SWAR digit windows (+7% instructions on doom: SGR params are 1–3 digits), loading values[0] on an empty list (store-forward block against clear's zeroing store once per CSI), and a ||-joined two-state guard (~8% csi cycles).

Guards

  • Kani push_run_equals_per_byte_primitives: push_run vs push_digit/next_slot over every 8-byte input (mutation-checked: a wrong clamp fails it).
  • Proptest csi_param_runs_match_per_byte_dispatch_at_any_chunking: CSI-dense streams (long digit runs, :, >16 params, private markers, C0/CAN/ESC mid-sequence) cut at random chunk boundaries, whole Parser and dispatch log compared against advance_byte.
  • Existing chunk-invariance test and Kani split-invariance proof pass; vt_parser fuzz target 33M runs in 240 s clean; just check green.

Review

pi (sol) on origin/main...HEAD: PASS, no findings.

Closes #417. `Params::push_run` fed every byte through `push_digit` / `next_slot`, so each digit reloaded `len`, re-checked it, and did a saturating multiply on the slot in memory, reading back the previous digit's store. It now keeps the open slot's value and the slot count in registers and writes the slot once per separator and once at the end of the run (`min(10v + d, u16::MAX)` in `u32` is the same fold as the per-byte saturating ops). The bulk arm in `advance` also takes the first parameter byte in `CsiEntry` (digit or `;`; a leading `:` stays per-byte because `start_empty` opens no subparam slot), saving one `advance_byte` round trip per CSI. ## Measurements Floor bench `measure_parse_into_grid_floor`, 35x106 (DOOM-fire capture geometry), median of 5 interleaved runs, load ~3, `instructions:u` over the bench's two passes: | payload | MiB/s base → new | instructions | |---|---|---| | doom (56 MiB DOOM-fire capture) | 218 → 241 (+10.8%) | 10350M → 9409M (−9.1%) | | truecolor (per-cell `38;2`/`48;2`) | 297 → 345 (+16.1%) | 5257M → 4476M (−14.9%) | | csi (kitten csi mix) | 143 → 154 (+7.3%) | 9315M → 8833M (−5.2%) | | sparse scroll (`y\r\n`) | 96.3 → 96.9 (+0.6%) | 30542M → 30961M (+1.4%) | The sparse scroll floor is flat over 10 interleaved runs (96.9 vs 96.5 MiB/s median, cycles within 0.5%), so the accepted cost in cost-maps does not grow. Rejected (numbers in the commit body and cost-maps): SWAR digit windows (+7% instructions on doom: SGR params are 1–3 digits), loading `values[0]` on an empty list (store-forward block against `clear`'s zeroing store once per CSI), and a `||`-joined two-state guard (~8% csi cycles). ## Guards - Kani `push_run_equals_per_byte_primitives`: `push_run` vs `push_digit`/`next_slot` over every 8-byte input (mutation-checked: a wrong clamp fails it). - Proptest `csi_param_runs_match_per_byte_dispatch_at_any_chunking`: CSI-dense streams (long digit runs, `:`, >16 params, private markers, C0/CAN/ESC mid-sequence) cut at random chunk boundaries, whole `Parser` and dispatch log compared against `advance_byte`. - Existing chunk-invariance test and Kani split-invariance proof pass; `vt_parser` fuzz target 33M runs in 240 s clean; `just check` green. ## Review pi (sol) on `origin/main...HEAD`: PASS, no findings.
Under DOOM-fire about 85% of the stream is `38;5;N` / `48;5;N` SGR
parameters, and `Params::push_run` was the largest self-time frame of
the doom floor bench (~20% of cycles and instructions). It fed each byte
through `push_digit` / `next_slot`, so every digit reloaded `len`,
re-checked it, and did a saturating multiply on the slot in memory,
reading back the store of the previous digit.

`push_run` now keeps the open slot's value and the slot count in
registers and writes the slot once per separator and once at the end of
the run. The saturation is `min(10v + d, u16::MAX)` in `u32`, which is
the same fold as the per-byte `saturating_mul(10).saturating_add(d)`.
The bulk arm in `advance` also takes the first parameter byte in
`CsiEntry`, where every SGR used to spend one `advance_byte` round trip
on `start_with`. A leading `:` stays per-byte because `start_empty`
opens no subparam slot. When the list is empty the run starts from 0
instead of loading `values[0]`: that load read `clear`'s wide zeroing
store, which blocked store forwarding once per CSI (6.4M blocked loads
vs 3.6M on the doom floor).

SWAR-decoding each digit run from an 8-byte window was rejected: at the
1–3 digit parameters SGR carries, the per-parameter mask, shift and
multiply cost more instructions than the digits they replace (+7% on
the doom floor). Spelling the arm as two state tests joined by `||`
instead of one `CsiEntry | CsiParam` range test cost the csi floor ~8%
of cycles against the single-range form.

Floor bench (`measure_parse_into_grid_floor`, 35x106 capture geometry,
median of 5 interleaved runs at load ~3, instructions:u over the bench's
two passes; base -> new):

  doom       218 -> 241 MiB/s (+10.8%)  10350M ->  9409M  (-9.1%)
  truecolor  297 -> 345 MiB/s (+16.1%)   5257M ->  4476M (-14.9%)
  csi        143 -> 154 MiB/s  (+7.3%)   9315M ->  8833M  (-5.2%)
  scroll      96 ->  97 MiB/s  (+0.6%)  30542M -> 30961M  (+1.4%)

The sparse scroll floor is flat over 10 interleaved runs (median 96.9
vs 96.5 MiB/s, cycles within 0.5%), so the recorded accepted cost of
the bulk arm does not grow.

Equivalence is pinned three ways: a Kani proof of `push_run` against the
per-byte primitives over every 8-byte input, a proptest comparing the
whole parser against `advance_byte` over CSI-dense streams cut at random
chunk boundaries, and the existing chunk-invariance test. The
`vt_parser` fuzz target ran 33M executions (240 s) clean.

Assisted-by: Claude Code
skills: record the register-decoded CSI parameter floor
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Has been cancelled
darwin / detect relevant changes (pull_request) Successful in 3s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 54s
darwin / publish felis (aarch64-darwin) (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 10m16s
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
windows / detect relevant changes (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
044bba2cc1
The SGR-flood cost map still described `push_run` as the per-byte
primitive loop. It now carries the floor figures after the register
decode and the `CsiEntry` bulk arm, and the three shapes that measured
worse (SWAR digit windows, loading the empty slot from `clear`'s store,
a two-test `||` state guard) so the next pass over the arm does not
re-run them.

Assisted-by: Claude Code
Merge remote-tracking branch 'origin/main' into vt/faster-param-runs
All checks were successful
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m6s
pr / nix flake check (pull_request) Successful in 34s
pr / wire schema is compatible with the base (pull_request) Successful in 12s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m22s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 4s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Successful in 2m18s
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 56s
darwin / publish felis (aarch64-darwin) (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m41s
windows / cargo nextest (Windows) (pull_request) Successful in 7m15s
windows / cargo clippy (Windows cross) (pull_request) Successful in 17s
windows / frontend smoke (Windows) (pull_request) Successful in 2m11s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
5d72af778d
natsukium deleted branch vt/faster-param-runs 2026-09-25 10:20:07 +09:00
Commenting is not possible because the repository is archived.
No description provided.