[Arrow] Typed zero-boxing column-major build for single-value primitive columns#18797
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #18797 +/- ##
==========================================
Coverage 64.80% 64.80%
Complexity 1347 1347
==========================================
Files 3392 3392
Lines 211648 211754 +106
Branches 33302 33341 +39
==========================================
+ Hits 137152 137222 +70
- Misses 63436 63442 +6
- Partials 11060 11090 +30
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
845a0ea to
3368c9d
Compare
| switch (fieldSpec.getDataType()) { | ||
| case INT: { | ||
| if (!columnReader.isInt()) { | ||
| return false; |
There was a problem hiding this comment.
When will we run into this ?
There was a problem hiding this comment.
If Arrow producers write a wider/narrower int than the Pinot schema (Arrow int64 or INT8/INT16 for an INT column), this condition kicks in
siddharthteotia
left a comment
There was a problem hiding this comment.
Thanks for addressing the original ask of optimizing away the boxing overhead. Couple of point
-
Can we put this behind a flag ? I mean the new path generates segments identical to previous path but just in case.
-
I don't see any log or metric to indicate which path fired. You may need it initially at least as we enable this and debug (if there are any issues)
-
Do we not need to add new tests to get coverage on the new path or is this already covered ?
|
Thanks @siddharthteotia. Correctness is already tested in existing tests, but let me add specific tests for internal type mismatches. Flag is not necessary because columnar builder is already opt-in only in Also adding a summary log on which path was fired. |
…ve columns The column-major build added in apache#18638 removed per-row GenericRow materialization but still consumes each value through the generic Object path (next() -> collect(Object) / indexOfSV(Object) / add(Object, dictId)), so every primitive is boxed on both the stats pass and the index pass. Wire the already-existing typed accessors and primitive sinks together for single-value INT/LONG/FLOAT/DOUBLE columns: read via ColumnReader.nextInt()/... (gated on isInt()/isLong()/...) and feed collect(int), indexOfSV(int), and IndexCreator.addInt(value, dictId) directly. Nulls route through the unchanged Object path for exact parity (including null-value-vector marking and default substitution); multi-value, BOOLEAN/TIMESTAMP, and every other type fall back unchanged. Also document the two-pass rewind/memory precondition on the init(config, ColumnReaderFactory) overload. Completes the per-primitive boxing removal tracked in apache#18629.
Emit one INFO summary per column-major build reporting how many single-value primitive columns took the typed allocation-free fast path versus how many fell back to the generic Object path (multi-value, non-primitive single-value, or a fast-path primitive whose reader served a different physical type than the schema). The two counts are incremented once per column at the point the path is chosen, so the summary cannot drift from the path actually taken. Add an Arrow integration test covering the INT64-source / INT-schema type-mismatch fallback that was previously unexercised.
31aad81 to
6f1c180
Compare
TL;DR: The column-major build added in #18638 removed per-row
GenericRowmaterialization, but it still consumes every value through the genericObjectpath — so each primitive is boxed twice, once on the stats pass and once on the index pass. This PR adds a typed, boxing-free fast path for single-valueINT/LONG/FLOAT/DOUBLEcolumns:ColumnReader.nextInt()→collect(int)/indexOfSV(int)→IndexCreator.addInt(value, dictId). Purely additive; nulls, multi-value, and every other type fall back to the unchanged Object path. No SPI changes, no new public API. It also logs a one-line fast-path/Object-path column summary per build.Tracks #18629. Builds on #18638 (column-major build) and the
ColumnReader/ColumnReaderFactorySPI (#16727).Problem
#18629 set out to build a segment from a columnar source without "the per-row
GenericRowallocation and per-primitive boxing." #18638 delivered the column-major path and removed theGenericRowallocation, but it routes values through the genericObjectAPI:So the per-primitive boxing #18629 aimed to remove is still on the hot path — and twice over, since
buildColumnar()reads every column once for stats and once for indexing. (This is the follow-up requested in the #18638 review.)What this PR does
The
ColumnReaderSPI (#16727) already exposes typed accessors (nextInt(),isInt(), …) and the downstream sinks already have primitive overloads —AbstractColumnStatisticsCollector.collect(int),SegmentDictionaryCreator.indexOfSV(int), andaddInt(value, dictId)onForwardIndexCreator/CombinedInvertedIndexCreator/DictionaryBasedInvertedIndexCreator. They were simply never wired to the column-major build. This PR wires them:ColumnarSegmentPreIndexStatsContainer) — when the reader can serve a single-value column as a primitive (isInt()/isLong()/…), read withnextInt()/… and feedcollect(int)/… directly.SegmentColumnarIndexCreator) — same dispatch:nextInt()→indexOfSV(int)→creator.addInt(value, dictId)across the column's index creators (creators with no typed override keep the boxing default and stay correct).Key choices:
FieldSpectype (INT/LONG/FLOAT/DOUBLE), soBOOLEAN(storedINT) andTIMESTAMP(storedLONG) — which require value coercion the typed read would skip — correctly stay on the Object path.isNextNull()is false.Net effect: for single-value primitive columns, neither build pass boxes — removing the remaining per-primitive boxing on both column reads.
Potential saving
Each column is read twice (stats pass + index pass), so the Object path boxes every single-value primitive value
2 × Ntimes for anN-row column. The typed path avoids, per such column:INT/FLOAT2 × N × 16 BLONG/DOUBLE2 × N × 24 BWrapper sizes assume a 64-bit JVM with compressed oops (
Integer/Longskip the −128..127 cache). For example a 1M-row column eliminates ~32 MB (INT/FLOAT) or ~48 MB (LONG/DOUBLE) of short-lived allocation and2 × Nboxing ops — allocation/GC pressure only; retained heap and segment size are unchanged.Scope
Single-value
INT/LONG/FLOAT/DOUBLEonly. The typed multi-value sinks (addIntMV,nextIntMV/MultiValueResult) already exist but are intentionally not wired here — the multi-value element-null contract is a distinct concern and is better handled on its own.Compatibility
Additive and behavior-preserving: no SPI changes, no new public API; the row-major path and the Object-path column-major fallback are untouched. The only non-fast-path edit documents the existing two-pass
rewind()precondition on theinit(config, ColumnReaderFactory)overload (the consumer that imposes it), leaving theColumnReaderFactorySPI itself generic.Observability
Emits one INFO summary per column-major build — how many single-value primitive columns took the typed fast path vs. the Object-path fallback (multi-value, non-primitive single-value, or a fast-path primitive whose reader served a different physical type than the schema). Counts are incremented once per column where the path is chosen, so the summary can't drift from the path taken. No SPI or public-API change.
Testing
The de-box is behavior-preserving (output-identical), so it's largely covered by the existing column-major equivalence suites; one new test is added for a previously-unexercised fallback:
ColumnarRowMajorEquivalenceTest— column-major vs. row-major over single-valueINT/LONG/FLOAT/DOUBLEwith ~10% nulls, asserting per-doc value and min/max/cardinality equality (typed dispatch + null branch).pinot-arrowcolumn-major suite (e.g.testRichMultiBatchEquivalence, INT with nulls across batches) exercises the typed path via an Arrow source.testSourceSchemaTypeMismatchInt64ToIntEquivalence(pinot-arrow): an INT64 Arrow source declared asINTin the schema declines the fast path and coerces via the Object path; asserts byte-for-byte parity with row-major.References
Tracks #18629 · builds on #18638 ·
ColumnReaderSPI #16727