iemejia opened a new issue, #3509:
URL: https://github.com/apache/parquet-java/issues/3509

   ### Describe the enhancement requested
   
   `BinaryPlainValuesReader.readBytes()` is the hot-path decoder for `BINARY` 
(and `STRING`) columns using `PLAIN` encoding. The current implementation 
funnels every length read through `BytesUtils.readIntLittleEndian(InputStream)` 
and every value slice through `ByteBufferInputStream.slice(int)`:
   
   ```java
   public Binary readBytes() {
     try {
       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);
     }
   }
   ```
   
   Two issues per value:
   
   1. `BytesUtils.readIntLittleEndian(InputStream)` calls `in.read()` four 
times. Each call goes through a `try` / `IOException` plumbing path and a 
virtual dispatch on `ByteBufferInputStream` (typically resolved as either 
`SingleBufferInputStream` or `MultiBufferInputStream`).
   2. `in.slice(length)` is also a virtual dispatch on `ByteBufferInputStream` 
for every value.
   
   If the page is materialised as a `MultiBufferInputStream` the cost is even 
higher because each slice may have to walk a buffer list.
   
   JMH (`BinaryEncodingBenchmark.decodePlain`, 100k values per invocation, JDK 
18, `-wi 5 -i 10 -f 3`, 30 samples) on master:
   
   | cardinality | stringLength | ops/s   |
   |------------:|-------------:|--------:|
   | HIGH        | 10           | 23.11M  |
   | HIGH        | 100          | 20.52M  |
   | HIGH        | 1000         | 7.07M   |
   | LOW         | 10           | 22.89M  |
   | LOW         | 100          | 20.35M  |
   | LOW         | 1000         | 6.28M   |
   
   ### Proposal
   
   Replace the `ByteBufferInputStream` field with a single `ByteBuffer` set up 
once in `initFromPage`:
   
   ```java
   @Override
   public void initFromPage(int valueCount, ByteBufferInputStream stream) 
throws IOException {
     int available = stream.available();
     this.buffer = available > 0
         ? stream.slice(available).order(ByteOrder.LITTLE_ENDIAN)
         : ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN);
   }
   
   @Override
   public Binary readBytes() {
     int length = buffer.getInt();
     ByteBuffer valueSlice = buffer.slice();
     valueSlice.limit(length);
     buffer.position(buffer.position() + length);
     return Binary.fromConstantByteBuffer(valueSlice);
   }
   ```
   
   The length prefix is now a single `ByteBuffer.getInt()` (one bounds check, 
no `IOException` plumbing, JIT-friendly intrinsic on little-endian buffers) and 
each value slice is a direct `ByteBuffer.slice()` instead of a virtual 
`ByteBufferInputStream.slice(int)`.
   
   The trade-off: when the input is a `MultiBufferInputStream` the upfront 
`stream.slice(available)` call may consolidate the page into a single fresh 
`ByteBuffer`. This is one allocation per page in exchange for inlined per-value 
reads, which is a clear win whenever the page contains more than a handful of 
values.
   
   Expected speedup (same JMH config):
   
   | cardinality | stringLength | Before  | After   | Δ       |
   |------------:|-------------:|--------:|--------:|--------:|
   | HIGH        | 10           | 23.11M  | 27.13M  | **+17.4% (1.17x)** |
   | HIGH        | 100          | 20.52M  | 22.20M  | **+8.2% (1.08x)**  |
   | HIGH        | 1000         | 7.07M   | 7.68M   | **+8.6% (1.09x)**  |
   | LOW         | 10           | 22.89M  | 26.46M  | **+15.6% (1.16x)** |
   | LOW         | 100          | 20.35M  | 22.16M  | **+8.9% (1.09x)**  |
   | LOW         | 1000         | 6.28M   | 7.50M   | **+19.4% (1.19x)** |
   
   Allocation per op is unchanged (~88 B/op = the returned `Binary` + the 
per-value `ByteBuffer` slice).
   
   The improvement is largest at small string lengths because the per-value 
fixed cost (length read + slice) dominates more there; at 1000-byte values the 
cost is increasingly dominated by the value-bytes copy/compare downstream 
rather than the read itself, but the gain is still ~9–19% even there.
   
   ### Scope
   
   - Single file change to 
`parquet-column/src/main/java/org/apache/parquet/column/values/plain/BinaryPlainValuesReader.java`.
   - No public-API change; only the implementation of `readBytes()`, `skip()`, 
and `initFromPage()` is rewritten.
   - All 573 `parquet-column` tests pass.
   
   ### Relation
   
   Part of a small series of focused performance PRs from work in 
[parquet-perf](https://github.com/iemejia/parquet-perf). Previous: #3494 
(PlainValuesReader), #3496 (PlainValuesWriter), #3500 (Binary.hashCode cache), 
#3504 (BSS writer), #3506 (BSS reader).


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to