Skip to content

[WIP][POC] Pfor encoding - #3595

Draft
prtkgaur wants to merge 13 commits into
apache:masterfrom
prtkgaur:pfor-encoding
Draft

prtkgaur wants to merge 13 commits into
apache:masterfrom
prtkgaur:pfor-encoding

Conversation

@prtkgaur

@prtkgaur prtkgaur commented Jun 3, 2026

Copy link
Copy Markdown

Rationale for this change

What changes are included in this PR?

Are these changes tested?

Are there any user-facing changes?

Implements the PFOR (Patched Frame of Reference) integer compression
encoding for INT32 and INT64 columns in the pfor package:
- PforConstants: header/vector sizes, max exceptions (65535)
- PforEncoderDecoder: histogram-based cost model for optimal bit width
- PforValuesWriter: IntPforValuesWriter + LongPforValuesWriter with
  vector-buffered encoding and interleaved page layout
- PforValuesReader: abstract base with lazy per-vector decoding
- PforValuesReaderForInt: INT32 decoder using BytePacker
- PforValuesReaderForLong: INT64 decoder using BytePackerForLong
Wires PFOR encoding into the parquet-java read/write pipeline:
- Encoding.java: add PFOR enum with INT32/INT64 reader dispatch
- ParquetProperties.java: add pforEnabled column property with
  isPforEnabled() and builder methods withPforEncoding()
- DefaultV2ValuesWriterFactory.java: PFOR takes priority over
  BYTE_STREAM_SPLIT and DELTA_BINARY_PACKED for INT32/INT64
- ParquetMetadataConverter.java: guard for PFOR until thrift spec
  is merged upstream
64 tests covering:
- PforEncoderDecoderTest: bit width utilities and histogram-based cost model
- PforBitPackingTest: round-trip correctness across bit widths 0-64, partial groups, page header format
- PforValuesEndToEndTest: full writer→reader pipeline including reset/reuse, skip, edge cases, random data
Benchmarks encode/decode throughput for int32/int64 across 8 data
distributions inspired by Snowflake's NumericComprBenchmark: constant,
sequential, small range, high-base-small-range (timestamps), with
outliers (exception path), random, TPC-DS date keys, TPC-DS quantity.

Uses junit-benchmarks (matches existing delta encoding benchmarks).
Prints compression ratios for all distributions during setup.
Excluded from normal test runs by surefire's benchmark exclusion.
Writer:
- Pre-allocate reusable buffers (deltasBuffer, excPosBuffer, excValBuffer,
  metadataBuf, packBuf, packPadBuf) in constructor instead of allocating
  new arrays on every encodeAndFlushVector call
- Replace ByteBuffer.allocate().order(LITTLE_ENDIAN) with manual byte
  shifts into reusable metadataBuf for vector info and exception writes
- Emit valid header for totalCount==0 (reader can distinguish empty page
  from missing encoding) instead of BytesInput.empty()

Reader:
- Add numElements > valuesCount validation (handles nullable columns where
  page row count > encoded values)
- Move getShortLE/getIntLE/getLongLE from private static in concrete
  readers to protected static in PforValuesReader base class
Tests cover:
- Bad packing mode, log vector size out of range, bad value byte width
- Negative num_elements, numElements > valuesCount
- Header-only page, truncated offset array, truncated vector data
- Corrupted offset pointing past buffer end
- Skip past end, negative skip, read past end
- Skip across vector boundaries (correctness check)
Pre-allocate reusable decode buffers (deltasBuffer, excPositionsBuffer,
unpackPadBuf, unpackTempBuf) in allocateDecodedBuffer instead of
allocating new arrays on every decodeVector call. Mirrors the writer-side
improvement from the previous commit.
getBytes() now emits a valid 7-byte header even when totalCount==0,
so the reader can distinguish an empty PFOR page from a missing
encoding. Update assertions from size==0 to size==PFOR_HEADER_SIZE.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has had no activity for at least 2 months. If you are still working on this change or plan to move it forward, please leave a comment or push a new commit so we know to keep it open. Otherwise, this PR will be closed automatically in about one month. Thank you for your contribution to Apache Parquet!

@github-actions github-actions Bot added the stale label Aug 3, 2026
They were added unformatted, so spotless:check fails on the branch as it stands.
Bit width, exception count, and exception positions all came off the wire and
sized reads and writes unchecked, so a corrupt page raised raw index errors.
parquet.enable.pfor turns PFOR on for INT32 and INT64 columns, following how
parquet.enable.bytestreamsplit exposes BYTE_STREAM_SPLIT: a constant, a getter
reading the key against the ParquetProperties default, an entry in the class
documentation, and a line in the properties the record writer builds. The
default is unchanged, so PFOR stays off unless a job asks for it.

Also wraps the long line the formatter rejects in ParquetMetadataConverter.
Every file here is byte-identical to its previous revision once comments are
stripped. This changes comments only.

Removed, by class:

- 23 headings of the form "// ===== INT32 Bit Width Coverage =====", and seven
  headings fenced above and below by a 75-dash rule. Neither shape occurs
  anywhere else in parquet-java: a census of *.java finds the boxed form in
  four files and the dashed form in one, and all five are ours. Every boxed
  heading restated the method names under it. The dashed ones named a group
  of attacks, so that text stays as a plain comment, which is what the
  upstream tests do.
- Seven arrow glyphs in comments, now "->". No other *.java file in the
  repository contains one.
- A block in findOptimalBitWidthForInt that argued with itself over five
  lines ("Actually:", "But let's compute it properly", "Correction:") and
  arrived back where it started. It now states the rule in two lines, the
  same rule findOptimalBitWidthForLong already carries.

Two corrections:

- The page-layout diagram in the reader and writer did not line up. Its
  third row spelled the multiplication sign as the HTML entity "×",
  seven source characters that render as one, leaving the cell a character
  too wide in the source and six too narrow once javadoc renders it. It is
  an "x" now and all four rows are 73 characters.
- "Interleaved" described the page layout in four files, and nothing in it
  is interleaved: the vectors are concatenated behind an offset array, and
  a vector's four sections follow one another. The word also names a
  bit-packing layout elsewhere in this work, so it points a reader at the
  wrong idea. The writer now says a vector carries its own info ahead of
  its data and can be decoded without reading another.

Verified with parquet-column spotless:check and the 89 PFOR tests.
The int32 search declared its exception counter as numElements and then
assigned numElements - bitsHist[0] to it before the loop that reads it, so
the first value was never used. The int64 search beside it declares the
counter once, with the value it needs, and this is now the same shape.

Behaviour is unchanged: nothing read the counter between the two statements.
The 89 PFOR tests pass, and this ships separately from the comment sweep in
the preceding commit so that one stays provably comment-only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants