| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…uffer I/O Replace ByteBufferInputStream and LittleEndianDataInputStream wrappers with direct ByteBuffer access for all PLAIN value readers and writers. Readers (PlainValuesReader, BooleanPlainValuesReader, BinaryPlainValuesReader, FixedLenByteArrayPlainValuesReader) now hold a little-endian ByteBuffer obtained from initFromPage() and call getInt/getLong/getFloat/getDouble directly, eliminating per-value stream overhead. Writers (PlainValuesWriter, BooleanPlainValuesWriter, FixedLenByteArrayPlainValuesWriter) write through CapacityByteArrayOutputStream's new writeInt/writeLong methods, which put values directly into the NIO slab buffer in little-endian order, avoiding temporary byte-array allocation. Supporting changes: - CapacityByteArrayOutputStream: allocate slabs with ByteOrder.LITTLE_ENDIAN, add writeInt(int) and writeLong(long) for single-value NIO writes. - BytesInput: add zero-copy writeTo(ByteBuffer) and toByteArray() using bulk ByteBuffer.get() instead of stream copy. - LittleEndianDataOutputStream: batch single-byte writes into single write(buf, 0, N) calls for writeShort/writeInt. Includes JMH benchmarks (PlainEncodingBenchmark, PlainDecodingBenchmark) covering all 7 primitive types for both encoding and decoding.
| int length = BytesUtils.readIntLittleEndian(in); | ||
| return Binary.fromConstantByteBuffer(in.slice(length)); | ||
| } catch (IOException | RuntimeException e) { | ||
| throw new ParquetDecodingException("could not read bytes at offset " + in.position(), e); |
There was a problem hiding this comment.
Should we keep the ParquetDecodingException? Otherwise we're throwing the raw {IOException,RuntimeException} which is a behavioral change.
Sorry, something went wrong.
There was a problem hiding this comment.
Done. Added try/catch wrapping RuntimeException (which covers BufferUnderflowException, IllegalArgumentException, etc.) into ParquetDecodingException in both readBytes() and skip().
Sorry, something went wrong.
| if (available > 0) { | ||
| this.buffer = stream.slice(available).order(ByteOrder.LITTLE_ENDIAN); | ||
| } else { | ||
| this.buffer = ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN); |
There was a problem hiding this comment.
Should we create a constant for the ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN);?
Sorry, something went wrong.
There was a problem hiding this comment.
Done. Extracted EMPTY_LE_BUFFER as a private static final read-only constant (ByteBuffer.allocate(0).order(LITTLE_ENDIAN).asReadOnlyBuffer()), used via .duplicate() in initFromPage. Same pattern applied in PlainValuesReader.
Sorry, something went wrong.
| try { | ||
| return Binary.fromConstantByteBuffer(in.slice(length)); | ||
| } catch (IOException | RuntimeException e) { | ||
| throw new ParquetDecodingException("could not read bytes at offset " + in.position(), e); |
There was a problem hiding this comment.
Same as above, should we keep the wrapped ParquetDecodingException?
Sorry, something went wrong.
There was a problem hiding this comment.
Done. Added try/catch wrapping in readBytes(), skip(), and skip(int n). Also using Math.multiplyExact(n, length) in skip(int n) to detect overflow.
Sorry, something went wrong.
| try { | ||
| skipBytesFully(n * 8); | ||
| } catch (IOException e) { | ||
| throw new ParquetDecodingException("could not skip " + n + " double values", e); |
There was a problem hiding this comment.
Same here, do we want to keep the ParquetDecodingException?
Sorry, something went wrong.
There was a problem hiding this comment.
Done. All readXxx() and skip(int n) methods across DoublePlainValuesReader, FloatPlainValuesReader, IntegerPlainValuesReader, and LongPlainValuesReader now wrap RuntimeException in ParquetDecodingException with descriptive messages matching the original error contract.
Sorry, something went wrong.
| public abstract class PlainValuesReader extends ValuesReader { | ||
| private static final Logger LOG = LoggerFactory.getLogger(PlainValuesReader.class); | ||
|
|
||
| protected LittleEndianDataInputStream in; |
There was a problem hiding this comment.
We should go through the deprecation cycle here, but is anything using this outside of the project itself?
Sorry, something went wrong.
There was a problem hiding this comment.
Good point. The old protected LittleEndianDataInputStream in field is now protected ByteBuffer buffer — the type change is binary-incompatible regardless, so a deprecation cycle wouldn't help external subclasses (they'd get a compile error either way). I searched the project and only internal subclasses (the 4 inner classes) access this field. I think this is acceptable given this class was never annotated @Public and the field type change makes deprecation impractical. WDYT?
Sorry, something went wrong.
| * mutable {@code BAOS.getBuf()}. | ||
| */ | ||
| @Override | ||
| public byte[] toByteArray() { |
There was a problem hiding this comment.
This overrides a deprecated API, as a follow-up we probably should move the internal calls to the new API:
@deprecated Use {@link #toByteBuffer(ByteBufferAllocator, Consumer)}
Sorry, something went wrong.
There was a problem hiding this comment.
Addressed in f0bdac6. The base-class toInputStream() now tries getInternalByteBuffer() first (zero-copy fast path) before falling back to the deprecated toByteBuffer(). Also added getInternalByteBuffer() and toInputStream() overrides to ByteArrayBytesInput so the byte-array-backed path is zero-copy too. Added @SuppressWarnings("deprecation") on the intentional overrides.
Sorry, something went wrong.
| public int getNextOffset() { | ||
| return in.getNextOffset(); | ||
| public void skip(int n) { | ||
| bitIndex += n; |
There was a problem hiding this comment.
Should we check for bounds, and throw a ParquetDecodingException in case of out of bounds?
Sorry, something went wrong.
There was a problem hiding this comment.
Done. Added a bitCount field (set to length * 8 in initFromPage) and an explicit bounds check in readBoolean() that throws ParquetDecodingException with a descriptive message when attempting to read beyond the page boundary.
Sorry, something went wrong.
| } catch (IOException e) { | ||
| throw new ParquetDecodingException("could not skip " + n + " double values", e); | ||
| } | ||
| buffer.position(buffer.position() + n * 8); |
There was a problem hiding this comment.
Should we use Math.multiplyExact here and below?
Sorry, something went wrong.
There was a problem hiding this comment.
Done. Applied Math.multiplyExact in all skip(int n) methods: Math.multiplyExact(n, 8) for double/long, Math.multiplyExact(n, 4) for float/int, and Math.multiplyExact(n, length) in FixedLenByteArrayPlainValuesReader. Overflow now produces an ArithmeticException which gets caught and wrapped in ParquetDecodingException.
Sorry, something went wrong.
- Wrap RuntimeException in ParquetDecodingException in all read/skip methods to preserve existing error contract - Extract EMPTY_LE_BUFFER constant for empty page initialization - Add bounds check in BooleanPlainValuesReader.readBoolean() - Use Math.multiplyExact in skip(int n) to detect overflow
… toByteArray/toByteBuffer - Base-class toInputStream() now tries getInternalByteBuffer() first for zero-copy path before falling back to deprecated toByteBuffer() - ByteArrayBytesInput: add getInternalByteBuffer() and toInputStream() overrides to avoid unnecessary copy through BAOS - Add @SuppressWarnings("deprecation") on intentional deprecated overrides
|
@Fokko another one ready for a last review / approval :) |
Sorry, something went wrong.
| throw new ParquetDecodingException("could not skip bytes at offset " + in.position(), e); | ||
| int length = buffer.getInt(); | ||
| buffer.position(buffer.position() + length); | ||
| } catch (RuntimeException e) { |
There was a problem hiding this comment.
Should we include IOException as well?
Sorry, something went wrong.
There was a problem hiding this comment.
The original code caught IOException | RuntimeException because it read through ByteBufferInputStream / BytesUtils.readIntLittleEndian(in) which throw checked IOException. The new code uses ByteBuffer.getInt() and ByteBuffer.position() directly -- these only throw RuntimeException subclasses (BufferUnderflowException, IllegalArgumentException), so IOException is no longer possible. Adding catch (IOException e) would actually be a compile error since nothing in the try block declares it. The same catch (RuntimeException e) pattern is used consistently across all the other readers (PlainValuesReader, FixedLenByteArrayPlainValuesReader).
Sorry, something went wrong.
|
Let's move this forward, thanks @iemejia for working on this! This is an impressive speed-up! |
Sorry, something went wrong.
|
Thank you @Fokko ! Great to see this landing and benefitting everyone. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Part of #3530 — Apache Parquet Java Performance Improvements
Summary
Replace ByteBufferInputStream and LittleEndianDataInputStream wrappers with direct ByteBuffer access for all PLAIN value readers and writers.
Readers (PlainValuesReader, BooleanPlainValuesReader, BinaryPlainValuesReader, FixedLenByteArrayPlainValuesReader): hold a little-endian ByteBuffer from initFromPage() and call getInt/getLong/getFloat/getDouble directly, eliminating per-value stream overhead.
Writers (PlainValuesWriter, BooleanPlainValuesWriter, FixedLenByteArrayPlainValuesWriter): write through CapacityByteArrayOutputStream's new writeInt/writeLong methods which put values directly into the NIO slab buffer in little-endian order, avoiding temporary byte-array allocation.
Supporting changes:
Includes JMH benchmarks (PlainEncodingBenchmark, PlainDecodingBenchmark) covering all 7 primitive types for both encoding and decoding.
Benchmark results
Environment: JDK 25.0.3 (Temurin), OpenJDK 64-Bit Server VM, JMH 1.37, Linux x86_64.
Decoding (100K values/iteration, 3 forks x 5 iterations, throughput mode):
Encoding:
(*) decodeLong/Double show JIT variance across forks (error bars >20%); true steady-state likely ~13x consistent with INT32/FLOAT.