Skip to content

perf(reader): untranspose fastlanes.delta straight into the output window - #348

Merged
dfa1 merged 1 commit into
mainfrom
perf/delta-scatter-chunk
Aug 7, 2026
Merged

perf(reader): untranspose fastlanes.delta straight into the output window#348
dfa1 merged 1 commit into
mainfrom
perf/delta-scatter-chunk

Conversation

@dfa1

@dfa1 dfa1 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Reconciles #343 with #345, both of which fixed #338 independently. I merged #345 without checking for an open PR on the issue first — that's on me, and this PR exists so #343's work isn't lost rather than because #345 needed changing.

Neither implementation dominated the other, so this takes both halves.

From #343

  • scatterChunk writes each untransposed value straight to its output index. That removes the chunk-sized untransposed staging buffer fix(reader): decode fastlanes.delta into the arena, not four heap long[] #345 kept, and the separate pass that sliced it. The bounds handling is the nice part: the leading chunk of an offset-sliced array maps to a negative output index and the trailing chunk runs past rowCount, and a single Long.compareUnsigned(at, rowCount) < 0 covers both, because a negative index reads as a huge unsigned value. The stores are a permutation scatter and never vectorize regardless, so the compare costs nothing the untranspose wasn't already paying.
  • out.asReadOnly() on the returned segment.
  • Element-indexed getAtIndex instead of hand-computed byte offsets.
  • The reader-module tests, which build the delta wire form directly, mirroring DeltaEncodingEncoder's transpose-then-per-lane-delta layout. This is the better call: the writer isn't on the reader's test classpath and never emits a non-zero offset, so fix(reader): decode fastlanes.delta into the arena, not four heap long[] #345 had to reach into the integration module to cover offset slicing. These cover it where it belongs, across offsets 1, 7, 1023, 1024, 1025 and 2000 — i.e. every relationship a window can have to a chunk boundary.

Kept from #345

  • Window-limited chunk iteration. perf(reader): decode fastlanes.delta without heap staging #343 walked all numChunks and discarded out-of-window stores per element; a one-chunk slice of a thousand-chunk column did a thousand chunks of work. With firstChunk/lastChunk it does one. The two mechanisms compose — the scatter's compare still handles the partial first and last chunk.
  • The metadata range guard. perf(reader): decode fastlanes.delta without heap staging #343 removed the heap arrays, which also removed the NegativeArraySizeException path, but left numChunks = (int)(deltasLen / CHUNK) unguarded: a deltas_len of Long.MAX_VALUE drives the loop ~9×10¹⁵ times. That's a hang, which is worse than the OutOfMemoryError it replaced.

Tests

Both suites, ten tests total: #343's wire-form round trips (all eight widths, chunk boundaries, offset slicing, short row counts) plus #345's malformed-metadata cases. The integration round-trip through the real file format stays as-is.

./mvnw verify green across all 17 modules.

Closes #343.

🤖 Generated with Claude Code

…ndow

Takes the better half of #343, which fixed #338 independently and in
parallel — I merged #345 for the same issue without checking for an open
PR first, so this reconciles the two rather than discarding one.

From #343:
- scatterChunk writes each untransposed value straight to its output
  index, dropping the chunk-sized `untransposed` buffer #345 staged it
  in and the separate pass that sliced it. The leading chunk of an
  offset-sliced array maps to a negative index and the trailing chunk
  runs past the row count; one `Long.compareUnsigned` covers both, since
  a negative index reads as a huge unsigned value. The stores are a
  permutation scatter and never vectorize regardless, so the compare
  costs nothing the untranspose was not already paying.
- The returned segment is read-only.
- Element-indexed `getAtIndex` instead of hand-computed byte offsets.
- Reader-module tests that build the delta wire form directly, mirroring
  DeltaEncodingEncoder's layout. These cover offset slicing where it
  belongs — the writer is not on the reader's test classpath and never
  emits a non-zero offset, so #345 had to reach into the integration
  module to cover the same shape.

Kept from #345:
- Only chunks overlapping the row window are reconstructed. #343 walked
  every chunk and discarded the out-of-window stores per element, so a
  one-chunk slice of a thousand-chunk column did a thousand chunks of
  work.
- The metadata range guard. Without it a `deltas_len` of Long.MAX_VALUE
  drives the chunk loop ~9e15 times — a hang, which is worse than the
  OutOfMemoryError it replaced.

Closes #343.

Co-Authored-By: Davide Angelocola <davide.angelocola@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dfa1
dfa1 merged commit 35e2f6e into main Aug 7, 2026
6 checks passed
@dfa1
dfa1 deleted the perf/delta-scatter-chunk branch August 7, 2026 11:18
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.

DeltaEncodingDecoder routes decode through four row-scaled heap long[] arrays

1 participant