Skip to content

perf(writer): ALP exponent search as Rust's alp crate (cascading write now faster than vortex-jni) - #469

Merged
dfa1 merged 1 commit into
mainfrom
perf/alp-exponent-search
Oct 9, 2026
Merged

dfa1 merged 1 commit into
mainfrom
perf/alp-exponent-search

Conversation

@dfa1

@dfa1 dfa1 commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Stacked on #468 (← #467 ← #466 ← #465 ← #464).

After #468, Java was still ~10% slower than vortex-jni. A CPU profile put the largest gap in ALP: Java ~184 ms vs Rust ~30 ms per OHLC write. The exponent search was the cause. It runs on every ALP trial and real encode, and Java searched 512 values where Rust's alp crate (0.0.4, pinned by vortex 0.86.1) searches 64.

What's ported (spiraldb/alp v0.0.4, src/alp/mod.rs, src/sample.rs)

  • SamplePlan::subsample: at most 64 values, 8 evenly spaced runs of 8
  • candidate order: every f <= e, e descending, starting from (0, 0); ties go to the smaller e - f
  • estimate_encoded_size_within: a candidate stops once its exceptions alone cost more than the best so far
  • fast_round ((x + 2^52 + 2^51) - (2^52 + 2^51), f32 2^23 + 2^22) instead of Math.round; Java's strict IEEE reproduces it bit for bit. Round-trip checks are bitwise, as is_eq.
  • exception slots filled with the first value that did encode (Rust's fill_value), not 0, so the integer child keeps its frame-of-reference range

The removed Java comment justified 512 samples by "precision drift" risk at small samples. Rust accepts that risk with 64; per the prime directive we take Rust's choice.

Numbers

before (#468) after vortex-jni
javaWriteCascading (5 forks) 0.767 ± 0.017 0.881 ± 0.008 0.848
OHLC 10M total 58,230,658 59,036,738 61,681,692
OHLC each price column ~5.99 MB (0.87×) ~6.19 MB (0.90×) ~6.91 MB
Taxi 2024-01 42,340,950 42,338,902 44,463,892

Every other OHLC column is byte-identical; the price columns move toward vortex-jni.

Testing

  • ./mvnw test -pl writer -am: 1995 tests (incl. the ALP round-trip suite)
  • ./mvnw verify -pl integration -am: 365 tests, 0 failures

🤖 Generated with Claude Code

https://claude.ai/code/session_015R8cf8YM8XFQqrkyhSAtYr

…e now faster than vortex-jni)

## perf(writer): search ALP exponents as Rust's alp crate does

Port find_best_exponents and encode from the `alp` crate 0.0.4, which
vortex 0.86.1 pins:
- search on SamplePlan::subsample: at most 64 values, 8 evenly spaced
  runs of 8 (Java sampled 512 strided values)
- every f <= e, e descending from (0, 0); ties go to the smaller e - f
- estimate_encoded_size_within: stop a candidate once its exceptions
  alone cost more than the best so far
- fast_round, (x + 2^52 + 2^51) - 2^52 - 2^51 (f32: 2^23 + 2^22), which
  Java's strict IEEE arithmetic reproduces bit for bit, instead of
  Math.round; round-trip checks bitwise, as is_eq
- exception slots filled with the first value that did encode, not 0,
  so the integer child keeps its frame of reference

The search runs on every ALP trial and real encode, so its sample size
was most of ALP's cost (Java ~184 ms vs Rust ~30 ms of CPU per OHLC
write).

Output (vortex-jni writing the same input):
  OHLC price columns ~5.99 MB -> ~6.19 MB each (jni ~6.91 MB:
  0.87x -> 0.90x); every other OHLC column byte-identical; total
  58,230,658 -> 59,036,738 (jni 61,681,692)
  Taxi 2024-01: 42,340,950 -> 42,338,902 bytes (jni 44,463,892)

JavaVsJniWriteBenchmark.javaWriteCascading, 5 forks:
  0.767 +- 0.017 -> 0.881 +- 0.008 ops/s (per fork 0.886 0.864 0.891 0.874 0.889)
  vortex-jni: 0.848 ops/s
  gc.alloc.rate.norm 2.50 -> 2.50 GB/op

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R8cf8YM8XFQqrkyhSAtYr
@dfa1
dfa1 force-pushed the perf/alp-exponent-search branch from a9aa931 to e07364f Compare October 9, 2026 08:01
@dfa1
dfa1 merged commit fed4f06 into main Oct 9, 2026
6 checks passed
@dfa1
dfa1 deleted the perf/alp-exponent-search branch October 9, 2026 08:05
dfa1 added a commit that referenced this pull request Oct 9, 2026
Re-measured on main after #464-#469 (Apple M5, Zulu JDK 25.0.2, vortex-jni
0.86.1): reads 3 forks, the cascading write 5 forks.

- README: an "at a glance" table (encodings, read, write, size vs
  vortex-jni) linking to the benchmark section.
- explanation.md: new read, write and big-file tables. Reads are now
  1.6-2.3x on numeric columns (vortex-jni got 3-6x faster since 0.79),
  ~4x on strings, 3.4x on the 3 GB scan. The write is on par (0.881 vs
  0.848), with the caveats from #474/#477 and how #464-#469 got there.
  Drops the 2026-07 write table, whose "plain" row no longer exists
  (defaults cascade since #458).
- withColumnEncoding: restricting candidates is now ~3.5x faster than
  the default cascade (24 -> 7 ms per 1M prices), not 6x -- the default
  path got cheaper. Updated in how-to, CHANGELOG and ColumnEncoding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R8cf8YM8XFQqrkyhSAtYr
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