Skip to content

GH-3516: Optimize DeltaByteArrayWriter and DeltaLengthByteArrayValuesWriter - #3517

Closed
iemejia wants to merge 2 commits into
apache:masterfrom
iemejia:perf-delta-bytearray-writer
Closed

GH-3516: Optimize DeltaByteArrayWriter and DeltaLengthByteArrayValuesWriter#3517
iemejia wants to merge 2 commits into
apache:masterfrom
iemejia:perf-delta-bytearray-writer

Conversation

@iemejia

Copy link
Copy Markdown
Member

Summary

Resolves#3516.

Two related changes in the DELTA_BYTE_ARRAY write path:

1. DeltaLengthByteArrayValuesWriter: drop the unused LittleEndianDataOutputStream wrapper

The class wrapped its CapacityByteArrayOutputStream with a LittleEndianDataOutputStream that was only used by Binary.writeTo() — an extra layer of dispatch on every value that never used any LE-specific functionality (writeInt/writeLong/etc.). Binary.writeTo(arrayOut) works directly with the underlying stream.

Also adds a new overload:

publicvoidwriteBytes(byte[] data, intoffset, intlength) {
lengthWriter.writeInteger(length);
arrayOut.write(data, offset, length);
}

so callers that already have the raw bytes can avoid allocating a Binary wrapper.

2. DeltaByteArrayWriter: eliminate per-value Binary.slice() allocation in the suffix path

Tightens the suffixWriter field type from ValuesWriter to DeltaLengthByteArrayValuesWriter (it's always constructed as one) so the new raw-bytes overload is callable. The suffix call becomes:

suffixWriter.writeBytes(vb, i, vb.length - i);

instead of suffixWriter.writeBytes(v.slice(i, vb.length - i)), eliminating the ByteArraySliceBackedBinary allocation per value plus a layer of virtual dispatch.

Benchmark results

From BinaryEncodingBenchmark.encodeDeltaByteArray / encodeDeltaLengthByteArray (added in #3512):

BenchmarkConfigurationmasterthis PRspeedup
encodeDeltaByteArrayLOW card, len=100.1028 µs0.0662 µs1.55x
encodeDeltaByteArrayHIGH card, len=100.1704 µs0.1124 µs1.52x
encodeDeltaByteArrayLOW card, len=1000.2079 µs0.1678 µs1.24x
encodeDeltaLengthByteArrayLOW card, len=100.0481 µs0.0397 µs1.21x
encodeDeltaLengthByteArrayLOW card, len=1000.1503 µs0.1374 µs1.09x

Long-string cases are flat or trivial — the per-value allocation is amortized away when each value is hundreds of bytes.

How to reproduce

The JMH benchmarks cited above are being added to parquet-benchmarks in #3512. Once that lands, reproduce with:

./mvnw clean package -pl parquet-benchmarks -DskipTests \
-Dspotless.check.skip=true -Drat.skip=true -Djapicmp.skip=true
java -jar parquet-benchmarks/target/parquet-benchmarks.jar \
'BinaryEncodingBenchmark.encodeDeltaByteArray|BinaryEncodingBenchmark.encodeDeltaLengthByteArray' \
-wi 5 -i 10 -f 3

Compare runs against master (baseline) and this branch (optimized).

Validation

  • parquet-column: 573 tests pass
  • Built with -Dspotless.check.skip=true -Drat.skip=true -Djapicmp.skip=true

User-facing changes

None. No public API change. No file format change.

The new DeltaLengthByteArrayValuesWriter.writeBytes(byte[], int, int) overload is added on top of the existing public API.

Closes#3516

Part of a small series of focused performance PRs from work in parquet-perf. Previous: #3494, #3496, #3500, #3504, #3506, #3510, #3514. Companion benchmarks contribution: #3512.

iemejia added a commit to iemejia/parquet-java that referenced this pull request May 1, 2026
Two small cleanups on the binary write side:
1. DeltaByteArrayWriter: replace v.getBytes() with v.copy().getBytesUnsafe()
to avoid the unconditional Arrays.copyOf that getBytes() performs for
ByteArrayBackedBinary. copy() is a no-op for constant Binaries, and
getBytesUnsafe() returns the backing array directly. For reused-buffer
Binaries (e.g. ByteBufferBackedBinary over a slab being mutated), copy()
still snapshots them so correctness is preserved.
2. FixedLenByteArrayPlainValuesWriter: drop the unused LittleEndianDataOutputStream
wrapper (only used to call Binary.writeTo(), which works directly with
the underlying CapacityByteArrayOutputStream). The trailing out.flush()
in getBytes() is also dead. Same pattern as apache#3517 fixed in
DeltaLengthByteArrayValuesWriter.
No public API change. No file format change.
Validation: parquet-column 573 tests pass. Built with
-Dspotless.check.skip=true -Drat.skip=true -Djapicmp.skip=true.
@iemejia
iemejiaforce-pushed the perf-delta-bytearray-writer branch 2 times, most recently from 7049836 to 7a47dd4CompareMay 1, 2026 22:07
iemejia added a commit to iemejia/parquet-java that referenced this pull request May 1, 2026
Two small cleanups on the binary write side:
1. DeltaByteArrayWriter: replace v.getBytes() with v.copy().getBytesUnsafe()
to avoid the unconditional Arrays.copyOf that getBytes() performs for
ByteArrayBackedBinary. copy() is a no-op for constant Binaries, and
getBytesUnsafe() returns the backing array directly. For reused-buffer
Binaries (e.g. ByteBufferBackedBinary over a slab being mutated), copy()
still snapshots them so correctness is preserved.
2. FixedLenByteArrayPlainValuesWriter: drop the unused LittleEndianDataOutputStream
wrapper (only used to call Binary.writeTo(), which works directly with
the underlying CapacityByteArrayOutputStream). The trailing out.flush()
in getBytes() is also dead. Same pattern as apache#3517 fixed in
DeltaLengthByteArrayValuesWriter.
No public API change. No file format change.
Validation: parquet-column 573 tests pass. Built with
-Dspotless.check.skip=true -Drat.skip=true -Djapicmp.skip=true.
@iemejia
iemejiaforce-pushed the perf-delta-bytearray-writer branch from 7a47dd4 to f79466cCompareMay 1, 2026 22:17
@FokkoFokko changed the title GH-3516: Optimize DeltaByteArrayWriter and DeltaLengthByteArrayValuesWriter (+33-55% encodeDeltaByteArray)GH-3516: Optimize DeltaByteArrayWriter and DeltaLengthByteArrayValuesWriterMay 6, 2026
@Fokko

Fokko commented May 6, 2026

Copy link
Copy Markdown
Contributor

@iemejia Can you fix the linting errors?

iemejia added 2 commits May 10, 2026 23:59
…luesWriter
Two related changes in the DELTA_BYTE_ARRAY write path:
1. DeltaLengthByteArrayValuesWriter: drop the unused LittleEndianDataOutputStream
wrapper. Binary.writeTo(arrayOut) works directly with the underlying
CapacityByteArrayOutputStream; the LE wrapper added an extra layer of
dispatch on every value but never used any LE functionality
(writeInt/writeLong/etc.). Add a new writeBytes(byte[], int, int) overload
so callers that already have the raw bytes can avoid allocating a Binary
wrapper.
2. DeltaByteArrayWriter: tighten suffixWriter field type to
DeltaLengthByteArrayValuesWriter (it's always constructed as one) so the
new writeBytes(byte[], int, int) overload is callable. Replace the suffix
call with the raw-bytes overload, eliminating the per-value Binary.slice()
allocation.
Benchmark (BinaryEncodingBenchmark, 100k BINARY values per invocation,
JMH -wi 3 -i 5 -f 1):
Benchmark Param Before (ops/s) After (ops/s) Improvement
encodeDeltaByteArray LOW/10 61,475,818 81,416,754 +32% (1.32x)
encodeDeltaByteArray LOW/100 34,759,755 45,186,617 +30% (1.30x)
encodeDeltaByteArray LOW/1000 5,386,922 6,532,850 +21% (1.21x)
encodeDeltaByteArray HIGH/10 56,799,595 78,966,929 +39% (1.39x)
encodeDeltaLengthByteArray LOW/10 129,447,876 136,657,079 +6%
encodeDeltaLengthByteArray HIGH/10 123,673,058 116,778,775 flat (noise)
Negative controls (encodePlain, encodeDictionary): unchanged within noise.
The DeltaByteArray path benefits most because it eliminates both the
Binary.slice() allocation per suffix and the OutputStream dispatch layer.
DeltaLengthByteArray gains are smaller since only the OutputStream wrapper
removal applies there.
No public API change. No file format change.
All 573 parquet-column tests pass.
Two small cleanups on the binary write side:
1. DeltaByteArrayWriter: replace v.getBytes() with v.copy().getBytesUnsafe()
to avoid the unconditional Arrays.copyOf that getBytes() performs for
ByteArrayBackedBinary. copy() is a no-op for constant Binaries, and
getBytesUnsafe() returns the backing array directly. For reused-buffer
Binaries (e.g. ByteBufferBackedBinary over a slab being mutated), copy()
still snapshots them so correctness is preserved.
2. FixedLenByteArrayPlainValuesWriter: drop the unused LittleEndianDataOutputStream
wrapper (only used to call Binary.writeTo(), which works directly with
the underlying CapacityByteArrayOutputStream). The trailing out.flush()
in getBytes() is also dead. Same pattern as apache#3517 fixed in
DeltaLengthByteArrayValuesWriter.
No public API change. No file format change.
Validation: parquet-column 573 tests pass. Built with
-Dspotless.check.skip=true -Drat.skip=true -Djapicmp.skip=true.
@iemejia
iemejiaforce-pushed the perf-delta-bytearray-writer branch from 936c34b to 4568f6cCompareMay 10, 2026 22:05
@iemejia
iemejia marked this pull request as draft May 15, 2026 09:35
@iemejia

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #3567.

I initially submitted a series of small, focused PRs thinking they'd be easier to review. In practice the sheer number (~16 PRs, with more pending) made things harder to follow — even for me. I've regrouped the changes by encoding type / performance area so that each PR is self-contained with its own benchmarks and test coverage, which should make review and performance analysis much more straightforward.

Apologies for the churn. If you've been reviewing this PR, please continue the discussion on #3567 which supersedes it. Thank you.

@iemejiaiemejia closed this May 17, 2026
@iemejia
iemejia deleted the perf-delta-bytearray-writer branch May 17, 2026 22:56
Sign up for freeto 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.

Optimize DeltaByteArrayWriter and DeltaLengthByteArrayValuesWriter: remove per-value allocation and LittleEndianDataOutputStream wrapper

2 participants

@iemejia@Fokko