Skip to content

perf: write offsets to compressed spill files as lengths - #26698

Closed
comphead wants to merge 2 commits into
apache:mainfrom
comphead:spill-offsets-as-lengths
Closed

comphead wants to merge 2 commits into
apache:mainfrom
comphead:spill-offsets-as-lengths

Conversation

@comphead

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Stacked on #26688, because lengths are not valid offsets and only read back when spill reads skip validation. Only the last commit is new here.

Rationale for this change

See #26627: spill files are mostly Utf8 offsets, which zstd and lz4 barely compress, while lengths repeat.

What changes are included in this PR?

  • Compressed spill files (lz4_frame and zstd) hold lengths instead of offsets for Utf8, Binary, List, Map and their large variants, at any depth through Struct and FixedSizeList. The buffer is [0, len_0, ..., len_(n-2), total]. The IPC writer reads only its first and last entries, to slice the values, so the stock encoder writes it as is.
  • The reader rebuilds the offsets of compressed record batches with a running sum, in place in the decompressed buffer. It copies only a buffer that the IPC writer stored raw because compressing it did not help.
  • Uncompressed spill files are unchanged.

The issue's reproducer (datafusion-cli -m 100m, median of 7 interleaved runs):

codec spilled, main -> PR query
zstd 32.0 -> 8.6 MB 314 -> 274 ms
lz4_frame 54.0 -> 16.8 MB 258 -> 250 ms

TPC-H SF1 sorts with Utf8 columns (schema_force_view_types = false, one partition, -m 200m, median of 5 runs):

query lz4_frame zstd
sort-tpch Q3 92.4 -> 78.0 MB, +0.8% 61.4 -> 50.3 MB, +1.1%
sort-tpch Q7 306.3 -> 229.5 MB, +0.6% 171.3 -> 122.0 MB, -2.5%
sort-tpch Q10 398.9 -> 308.7 MB, +3.0% 228.2 -> 168.4 MB, -2.7%

In the spill_io benchmark against #26688, files with strings are 27% to 41% smaller. Writing plus reading them takes up to 7% less time with zstd and about 8.5% more with lz4, because lz4 stored the incompressible offsets raw and now compresses the lengths.

Open question: should lz4_frame keep this, or only zstd?

What is the testing strategy for this PR?

  • test_spill_offsets_as_lengths_round_trip spills every rewritten type, nested and sliced, with all three codecs. Its empty and two-row batches cover buffers the IPC writer stores raw.
  • test_spill_offsets_as_lengths_compress checks that a zstd spill of similar strings is under a quarter of plain zstd IPC.
  • test_restore_offsets_in_place_unless_shared checks that the restore reuses an unshared buffer.

Are there any user-facing changes?

Smaller spill files with lz4_frame or zstd. No configuration or API changes.

apache#15454 stopped re-validating spill files on read. apache#21882 moved the reader to
`StreamDecoder`, whose `with_skip_validation` only applies to dictionary
batches, so every offset and string of a spilled record batch is validated
again. Decode the framed messages with `RecordBatchDecoder` instead, which
honors the flag.
Closes apache#26627.

Offsets are a running total, so zstd keeps about 2.5 of every 4 bytes and
lz4 all of them, while the lengths of similar values repeat. Compressed
spill files now hold the lengths of `Utf8`, `Binary`, `List` and `Map`
arrays, their large variants included, at any nesting depth through `Struct`
and `FixedSizeList`. The reader rebuilds the offsets with a running sum, in
place in the decompressed buffer.
@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label Oct 24, 2026
@comphead comphead closed this Oct 24, 2026
@comphead

Copy link
Copy Markdown
Contributor Author

doesn't seem to benefit a lot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spill files are mostly Utf8 offsets, which zstd and lz4 barely compress

1 participant