Skip to content

perf(fsst): close the vortex-jni gap on encode and decode hot paths#300

Open
dfa1 wants to merge 1 commit into
mainfrom
fsst-hot-path-perf
Open

perf(fsst): close the vortex-jni gap on encode and decode hot paths#300
dfa1 wants to merge 1 commit into
mainfrom
fsst-hot-path-perf

Conversation

@dfa1

@dfa1 dfa1 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Follow-up hot-path pass over the #287 FSST rewrite, iterated against JavaVsJniFsstBenchmark with per-round profiling (-prof stack:lines=10).

Results (-f 3, same machine/run for both sides)

Benchmark Before After vs vortex-jni
javaFsstEncode 1.848 ops/s 3.059 ± 0.430 ops/s 1.6x slower → parity (jni 2.854 ± 0.289)
javaFsstDecode 27.137 ops/s 32.924 ± 0.824 ops/s 1.3x slower → 1.16x faster (jni 28.353 ± 5.283)

Changes

fsst module (encode hot loop):

  • Compressor.compress (both overloads): single unaligned LE 8-byte word load while ≥8 input bytes remain (byteArrayViewVarHandle / LE_LONG); byte-by-byte loadWord retained for the tail only. The fast path drops the over-long-match guard (a ≤8-byte match cannot cross end there) and takes the escape literal from the already-loaded word.
  • LossyPerfectHashTable: lookup returns a packed code << 8 | length int (0 = miss) instead of allocating a HashMatch record; table stored as two adjacent longs per slot (symbol + ignoredBits<<16|code<<8|length, the paper's C layout) so a lookup touches one cache line instead of three parallel arrays; empty slots are all-zero and self-answer no-match.
  • ShortCodeTable/Matcher: pre-packed no-match sentinel makes the fallback one branch-free array read; longestMatch evaluates both tables and selects (cmov-friendly). codeOf(noMatch) == NO_CODE semantics preserved.

writer adapter:

  • FsstEncodingEncoder: all rows compress back-to-back into one shared scratch buffer (was a fresh scratch + exact-size copy per row — 2M allocations per 1M-row chunk write), wire-code remap in a single pass, offsets from rowEnds.

reader adapter:

  • FsstEncodingDecoder: output offsets built as per-ptype prefix sums over the uncompressed-lengths child, and the code stream decoded in 256-row batches (per-row ptype switch and two i % cap broadcast modulos removed from the 1M-row loops). Batching is deliberate: a single whole-chunk decompress call measured ~1.8x slower per byte (OSR-compiled mega-loop) — documented on ROWS_PER_DECODE_BATCH.
  • New malformed-input guards per the security contract (empty children, invalid code offsets, row-count and decoded-length overflow, decoded-vs-claimed length mismatch), each with a negative test in the new FsstEncodingDecoderTest (10 tests).

Verification

  • ./mvnw verify green (all modules, integration tests included) — Rust interop (JavaWritesRustReads, RustWritesJavaReads) confirms wire output unchanged.
  • vortex-reviewer pass: APPROVE; its should-fix (negative tests for the new guards) and nit (>2GB decoded-length guard) are included here.
  • Docs: CHANGELOG entry with the -f 3 table; TODO FSST follow-ups refreshed (stale 1.6x-gap phrasing).

🤖 Generated with Claude Code

Compressor: single unaligned 8-byte word loads on the fast path (byte-by-byte
loadWord kept for the <8-byte tail), escape literal taken from the loaded word.
LossyPerfectHashTable: packed-int lookup over a two-longs-per-slot table (the
paper's C layout) — one cache line per lookup instead of three parallel-array
reads; empty slots self-answer no-match with no occupancy flag or sentinel.
ShortCodeTable/Matcher: pre-packed no-match slots, one array read per fallback.
FsstEncodingEncoder: all rows compress into one shared scratch (was two heap
allocations plus an extra copy per row), one-pass wire-code remap.
FsstEncodingDecoder: prefix-sum output offsets from the lengths child and
256-row batched decompression (per-row switch/modulo removed; whole-chunk
single call measured 1.8x slower via OSR-compiled mega-loop), plus new
malformed-input guards (empty children, invalid offsets, row-count and
decoded-length overflow, decoded-vs-claimed mismatch) with negative tests.

JavaVsJniFsstBenchmark -f 3: encode 1.848 -> 3.059 +- 0.430 ops/s (jni 2.854,
parity), decode 27.137 -> 32.924 +- 0.824 ops/s (jni 28.353, 1.16x faster).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dfa1
dfa1 force-pushed the fsst-hot-path-perf branch from 130927f to 3e186af Compare July 22, 2026 06:30
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