GH-3772: Bulk skip in RunLengthBitPackingHybridDecoder / DictionaryValuesReader - #3773
abstractdog wants to merge 4 commits into
Conversation
5179bd9 to
f3246b5
Compare
|
can I ask for a review of this? made Hive-side benchmark with this change, wonderful results for dictionary columns, see details in apache/hive#6758 (comment) |
divjotarora
left a comment
There was a problem hiding this comment.
Nice optimization @abstractdog! One small comment to maintain today's exception behavior in the empty dictionary case.
There was a problem hiding this comment.
DictionaryValuesReader#initFromPage has this code:
public void initFromPage(int valueCount, ByteBufferInputStream stream) throws IOException {
this.in = stream.remainingStream();
if (in.available() > 0) {
LOG.debug("init from page at offset {} for length {}", stream.position(), stream.available());
int bitWidth = BytesUtils.readIntLittleEndianOnOneByte(in);
LOG.debug("bit width {}", bitWidth);
decoder = new RunLengthBitPackingHybridDecoder(bitWidth, in);
} else {
decoder = new RunLengthBitPackingHybridDecoder(1, in) {
@Override
public int readInt() throws IOException {
throw new IOException("Attempt to read from empty page");
}
};
}
}
Specifically in the else, we create a special RunLengthBitPackingHybridDecoder for empty dictionary pages. Should we override that decoder's skipInts to preserve the same IOException behavior when skipping? Otherwise, we bypass readInt and call readNext in the skip path, which throws IllegalArgumentException.
There was a problem hiding this comment.
makes sense @divjotarora , good catch, thanks! addressed it
f3246b5 to
e359972
Compare
divjotarora
left a comment
There was a problem hiding this comment.
Nice change @abstractdog!
| * same guard: without it, readNext() would hit the empty stream and throw a raw | ||
| * IllegalArgumentException that {@link DictionaryValuesReader#skip(int)} does not wrap. | ||
| * Skipping zero values reads nothing and stays silent, matching the loop that | ||
| * {@link org.apache.parquet.column.values.ValuesReader#skip(int)} used to run. |
There was a problem hiding this comment.
I prefer not to have comments like "used to run" in code as it's codifying old behavior that's been replaced. Is this actually necessary here? Seems fine to just say "Skipping zero values does nothing and stays silent" without any mention of the old loop.
This is personal preference, so feel free to ignore if you disagree / defer to the maintainers of the repo.
There was a problem hiding this comment.
agree, tend to take care of the same, missed this time:
80ecda7
…naryValuesReader Add skipInts(int n) to RunLengthBitPackingHybridDecoder. It re-uses the existing readNext() to load each run and then advances currentCount by min(n, currentCount) instead of returning values one-by-one via readInt(). Runs are decoded the same way as before -- the win is dropping the per-value mode switch, array-index arithmetic and method-call overhead that readInt() pays for each value the caller is going to throw away. Propagate to the two ValuesReader wrappers (DictionaryValuesReader, RunLengthBitPackingHybridValuesReader) so callers going through the public ValuesReader.skip(int) contract get the fast path. Motivation: dictionary-encoded columns are ubiquitous in production Parquet, and the default ValuesReader.skip(int) is a naive loop over skip() -- which for dict columns is a RunLengthBitPackingHybridDecoder readInt(). Filter-driven read paths (column-index row ranges, Hive ProbeDecode, arbitrary row-skip) pay that cost per skipped row even when the values are being thrown away. Bench (parquet-benchmarks / RleSkipBenchmark, thrpt, 1 fork, 3x1s warmup, 5x1s measure, 100k values/op, ops/s of individual values): pattern=rle bitWidth=8: 1.38 B/s -> 103.2 B/s (~75x) pattern=packed bitWidth=8: 0.89 B/s -> 4.12 B/s (~4.6x) pattern=mixed bitWidth=8: 0.96 B/s -> 6.85 B/s (~7.1x) RLE runs dominate because a whole run is consumed with a single currentCount decrement; bit-packed runs still get fully unpacked, so the gain there is just the readInt() overhead avoided per value. Tests: TestRunLengthBitPackingHybridDecoderSkip covers RLE-only, PACKED-only, mixed, zero-skip, full-skip, partial-run skip, bitWidth=0, and randomized skip/read alternation across 4K values. All 700 parquet-column tests still pass.
The empty-page decoder in DictionaryValuesReader#initFromPage overrode only
readInt(), so skip(int) -> skipInts(n) bypassed the guard, reached
RunLengthBitPackingHybridDecoder#readNext() and threw a raw
IllegalArgumentException ("Reading past RLE/BitPacking stream") that the
catch (IOException) in skip(int) does not wrap. Before bulk skip,
ValuesReader.skip(int) looped over skip() -> readInt() and produced a
ParquetDecodingException.
Override skipInts() with the same guard. Skipping zero values reads nothing
and stays silent, matching the zero-iteration loop it replaces.
e359972 to
80ecda7
Compare
Rationale for this change
ValuesReader.skip(int n)defaults to a naive loop ofskip()calls. For dictionary-encoded columns eachskip()bottoms out inRunLengthBitPackingHybridDecoder.readInt()— a mode switch, array indexing, and a value the caller immediately discards. Any filter-then-skip path (column-index row ranges, hash-join probe filtering, runtime filters) pays this cost per skipped row.RleSkipBenchmark, JMH throughput, JDK 17, 100 k values/op:readInt()loopskipIntsWhat changes are included in this PR?
RunLengthBitPackingHybridDecoder.skipInts(int)— re-usesreadNext()per run, then advancescurrentCountbymin(n, currentCount)instead of walking every value throughreadInt().skip(int)overrides onDictionaryValuesReaderandRunLengthBitPackingHybridValuesReaderdelegating todecoder.skipInts(n).parquet-benchmarks / RleSkipBenchmarkJMH benchmark.readNext()and theValuesReader.skip(int)default are unchanged.Are these changes tested?
TestRunLengthBitPackingHybridDecoderSkipcovers RLE-only, PACKED-only, mixed, zero-skip, full-skip, mid-run partial skip,bitWidth = 0, and a randomised 4 K-value read/skip interleaving cross-checked against areadInt()reference. Allparquet-columntests pass.Are there any user-facing changes?
No. Additive and binary-compatible: no signatures change;
skipInts(n)is semantically identical to N discardedreadInt()s. Existing callers ofValuesReader.skip(int)pick up the fast path with no code change.