Skip to content

perf(decode): fast-path 1/2-byte varints (+12% codec decode) - #127

Merged
facontidavide merged 4 commits into
mainfrom
perf/decode-optimization
Jun 21, 2026
Merged

perf(decode): fast-path 1/2-byte varints (+12% codec decode)#127
facontidavide merged 4 commits into
mainfrom
perf/decode-optimization

Conversation

@facontidavide

Copy link
Copy Markdown
Owner

Summary

decodeVarint is the hottest primitive on the decode path (~8 calls per point for a typical LIDAR cloud). It ran a general loop with a per-byte bounds check and two overflow checks for every value. This PR adds branch-predicted 1-byte and 2-byte fast paths — covering the vast majority of real deltas — while keeping the careful general path for 3+ byte varints and all truncation/overflow/NaN semantics unchanged.

The motivation came from profiling decode with perf: after a stage of decodeVarint calls dominate both the integer fields and the inlined XYZ float path, the redundant per-byte checks were a measurable tax.

Results (codec-only decode, DATA/ouster_os1.mcap, 94 msgs)

Mode Before After Δ
V4 ~1462 MB/s ~1643 MB/s +12.4%
V5 ~1303 MB/s ~1464 MB/s +12.4%

Measured with mcap_codec_benchmark --decode-replay --mode <V4/V5> --decode-repeat 40 (5 runs each, tight ±0.5% variance). The general (3+ byte) path is the original code byte-for-byte; only the 1- and 2-byte paths are new.

Correctness

  • New differential test DecodeVarintMatchesOracleExhaustiveAndRandom: compares the optimized decoder against an in-test oracle (the original implementation, verbatim) over all 1- and 2-byte inputs × every truncation length, a boundary sweep of 3-byte inputs, and 200k randomized longer/malformed inputs. Zero divergence in value, byte count, or throw/no-throw. Runs ~1s under Debug/ASan.
  • Decoded-output FNV-1a fingerprint over the real bag is unchanged (0x75b0cc1fbc0457e9) — exposed via a new mcap_codec_benchmark --hash correctness gate.
  • Full ctest green. Independently reviewed (adversarial differential sweep up to 67M cases under ASan, zero mismatches; bounds-safety verified — buf[1] is read only under a short-circuited max_size >= 2 guard, so no out-of-bounds read on truncated input).

Scope & notes

  • The encoder (encodeVarint64) and the wire format are untouched — round-trip contract preserved.
  • Explored and rejected during this work (no gain / regressions, all reverted): SIMD-izing the FloatN int→float convert (store-forwarding stall, −2%), devirtualizing per-point dispatch (V5 −4%), decoder caching (0% wall-clock — a profiler artifact), and a 3-byte fast path (−1.5%, the case is too rare here). The dominant remaining decode hotspot (FloatN, ~42%) is data-dependency-bound; further decode gains would need a SIMD-decodable varint wire format (e.g. Stream VByte) — a separate, larger change.
  • Pre-existing, not introduced here (flagged by review for a possible follow-up): signed-shift UB in encodeVarint64's zigzag (value << 1 on negative), and the INT64_MIN ↔ NaN-marker collision. Neither arises from real quantized/delta point data.

Commits

  1. perf(decode): fast-path 1/2-byte varints in decodeVarint (+ --hash benchmark tooling)
  2. test(decode): differential oracle test for the fast paths
  3. refactor(decode): address review — trim test sweep to 200k, reject --hash without --decode-replay

🤖 Generated with Claude Code

facontidavide and others added 4 commits June 21, 2026 20:05
decodeVarint ran a general loop with a per-byte bounds check and two
overflow checks for every value. Add branch-predicted 1-byte and 2-byte
fast paths (covering the vast majority of real deltas); keep the careful
general path for 3+ byte varints and all truncation/overflow/NaN
semantics unchanged.

Decode throughput on DATA/ouster_os1.mcap (codec-only, 94 msgs):
  V4: ~1462 -> ~1645 MB/s (+12.5%)
  V5: ~1303 -> ~1476 MB/s (+13.3%)

Decoded-output FNV-1a fingerprint unchanged (0x75b0cc1fbc0457e9); ctest green.

Also adds a --hash correctness fingerprint to mcap_codec_benchmark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds DecodeVarintMatchesOracleExhaustiveAndRandom: compares the optimized
decodeVarint against an in-test oracle (the original loop-only implementation,
verbatim) over all 1- and 2-byte prefixes with every truncation length, a
boundary sweep of 3-byte prefixes, and 1M randomized longer/malformed inputs.
Any divergence in value, byte count, or throw/no-throw fails the test.

Runs in ~2s under Debug+ASan. Proves the 1/2-byte fast paths preserve exact
varint semantics (truncation, overflow, NaN marker, zigzag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From /pr-review + /simplify:
- Reduce the differential test's random sweep 1M -> 200k. The 1/2-byte fast
  paths are already proven exhaustively and the 3+ byte path is original code,
  so the sweep is supplementary; this keeps the ASan/Debug ctest ~1s.
- Reject --hash without --decode-replay. The fingerprint pass lives inside the
  decode-replay block, so --hash alone silently printed nothing — a footgun for
  a correctness gate. Now errors clearly.

ctest green; decoded-output fingerprint unchanged (0x75b0cc1fbc0457e9).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facontidavide
facontidavide merged commit f15ef13 into main Jun 21, 2026
@facontidavide
facontidavide deleted the perf/decode-optimization branch June 21, 2026 18:57
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.

1 participant