[SPARK-57111][SQL] Use intrinsic bulk-fill APIs for putNotNulls#56156
[SPARK-57111][SQL] Use intrinsic bulk-fill APIs for putNotNulls#56156viirya wants to merge 4 commits into
Conversation
|
Does this PR include the following? |
|
#56159 is for baseline numbers. This PR adds a more case to the benchmark so no baseline. |
…BulkFillBenchmark ### What changes were proposed in this pull request? Adds a `putNotNulls` case to `WritableColumnVectorBulkFillBenchmark` (introduced in SPARK-57042 / #56084). The case mirrors the existing `putNulls` case and exercises both `OnHeapColumnVector.putNotNulls` and `OffHeapColumnVector.putNotNulls` across the standard count sweep (1, 8, 64, 512, 4096, 65536). `putNotNulls` early-outs unless `numNulls > 0`, so the OnHeap / OffHeap factories seed one null into the column vector at setup before the measured loop begins. ### Why are the changes needed? `putNotNulls` is the inverse of `putNulls` and runs once per batch from `WritableColumnVector.reset()` when `numNulls > 0`. SPARK-57111 (#56156) is switching its implementation from a per-byte loop to `Arrays.fill` / `Platform.setMemory` (with the `SET_MEMORY_THRESHOLD` fallback). Landing the benchmark case in master first means SPARK-57111 will produce a like-for-like `Run benchmarks` GHA baseline → patched comparison, the same way SPARK-57042 / #56084 enabled the comparison for SPARK-57036 / #56082. ### Does this PR introduce _any_ user-facing change? No. Benchmark-only. ### How was this patch tested? The benchmark compiles and runs: ``` build/sbt "sql/Test/runMain org.apache.spark.sql.execution.vectorized.WritableColumnVectorBulkFillBenchmark" ``` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 4.7) Closes #56159 from viirya/SPARK-57112. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
…BulkFillBenchmark ### What changes were proposed in this pull request? Adds a `putNotNulls` case to `WritableColumnVectorBulkFillBenchmark` (introduced in SPARK-57042 / #56084). The case mirrors the existing `putNulls` case and exercises both `OnHeapColumnVector.putNotNulls` and `OffHeapColumnVector.putNotNulls` across the standard count sweep (1, 8, 64, 512, 4096, 65536). `putNotNulls` early-outs unless `numNulls > 0`, so the OnHeap / OffHeap factories seed one null into the column vector at setup before the measured loop begins. ### Why are the changes needed? `putNotNulls` is the inverse of `putNulls` and runs once per batch from `WritableColumnVector.reset()` when `numNulls > 0`. SPARK-57111 (#56156) is switching its implementation from a per-byte loop to `Arrays.fill` / `Platform.setMemory` (with the `SET_MEMORY_THRESHOLD` fallback). Landing the benchmark case in master first means SPARK-57111 will produce a like-for-like `Run benchmarks` GHA baseline → patched comparison, the same way SPARK-57042 / #56084 enabled the comparison for SPARK-57036 / #56082. ### Does this PR introduce _any_ user-facing change? No. Benchmark-only. ### How was this patch tested? The benchmark compiles and runs: ``` build/sbt "sql/Test/runMain org.apache.spark.sql.execution.vectorized.WritableColumnVectorBulkFillBenchmark" ``` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 4.7) Closes #56159 from viirya/SPARK-57112. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Dongjoon Hyun <dongjoon@apache.org> (cherry picked from commit 6b727c9) Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
Follow-up to SPARK-57024 / SPARK-57036.
WritableColumnVector.putNotNulls(rowId, count) clears a run of the
nulls bitmap; it is called once per batch from
WritableColumnVector.reset() when numNulls > 0, and from the
appendNotNulls() path. Both OnHeap and OffHeap implementations were
per-element byte loops — the same degenerate pattern fixed for
putNulls in SPARK-57024 and for putBytes / putBooleans in SPARK-57036.
This change applies the same intrinsic substitutions:
- OnHeap.putNotNulls -> Arrays.fill(byte[], ..., (byte) 0)
- OffHeap.putNotNulls -> Platform.setMemory(addr, (byte) 0, count)
with the existing SET_MEMORY_THRESHOLD = 128 fallback to an inline
byte loop for small counts (avoids the JNI fixed-cost regression
setMemory has at very short fills, same as putNulls).
Also extends WritableColumnVectorBulkFillBenchmark with a putNotNulls
case mirroring the existing putNulls case, so the change has direct
before/after numbers.
Co-authored-by: Claude Code
….13, split 1 of 1)
….13, split 1 of 1)
….13, split 1 of 1)
| OnHeap 0 0 0 2586.6 0.4 1.0X | ||
| OffHeap 0 0 0 2550.2 0.4 1.0X | ||
| OnHeap 0 0 0 2803.9 0.4 1.0X | ||
| OffHeap 0 0 0 1514.9 0.7 0.5X |
There was a problem hiding this comment.
The relative ratio seems to be change. Does this mean OffHeap become 2x slower?
| OnHeap 0 0 0 1933.2 0.5 1.0X | ||
| OffHeap 0 0 0 1162.5 0.9 0.6X | ||
| OnHeap 0 0 0 2034.0 0.5 1.0X | ||
| OffHeap 0 0 0 465.4 2.1 0.2X |
There was a problem hiding this comment.
This also seems to show that OffHeap becomes slower than before. (0.6X -> 0.2X)
| OnHeap 2 2 0 2767.4 0.4 1.0X | ||
| OffHeap 3 3 0 1407.1 0.7 0.5X | ||
| OnHeap 1 1 0 3100.7 0.3 1.0X | ||
| OffHeap 0 0 0 36768.3 0.0 11.9X |
| OnHeap 24 24 0 2791.4 0.4 1.0X | ||
| OffHeap 48 48 0 1410.1 0.7 0.5X | ||
| OnHeap 21 22 2 3153.0 0.3 1.0X | ||
| OffHeap 1 1 0 49509.1 0.0 15.7X |
|
@dongjoon-hyun Thanks for going through the numbers. Quick triage: Lines 290 / 297 — Lines 24 and 276 — I dug into these and I'm fairly sure both are GHA single-run noise rather than regressions caused by this PR:
Sweeping the four OffHeap byte-fill methods at small counts side-by-side makes the noise pattern clearer (JDK 21, Rate M/s, OffHeap only):
So: lines 290 / 297 are real, lines 24 / 276 (and the |
|
Feel free to merge this PR, @viirya . I'm okay to merge this PR with AS-IS status because the big-win is clearly verified by the benchmark. |
|
Thank you @dongjoon-hyun. |
|
Note that the failed test org.apache.spark.sql.jdbc.v2.OracleIntegrationSuite is a known flaky one and not related to this change. |
### What changes were proposed in this pull request? Follow-up to #56072 (SPARK-57024) and #56082 (SPARK-57036). `WritableColumnVector.putNotNulls(rowId, count)` clears a run of the nulls bitmap. It is called once per batch from `WritableColumnVector.reset()` (when `numNulls > 0`) and from the `appendNotNulls()` path. Both OnHeap and OffHeap implementations are per-element byte loops — the same degenerate pattern that SPARK-57024 fixed for `putNulls` and SPARK-57036 fixed for `putBytes` / `putBooleans`. This change applies the same intrinsic substitutions: | Method | Substitution | | --- | --- | | `OnHeapColumnVector.putNotNulls(rowId, count)` | `Arrays.fill(byte[], ..., (byte) 0)` | | `OffHeapColumnVector.putNotNulls(rowId, count)` | `Platform.setMemory(addr, (byte) 0, count)` with small-count fallback | The OffHeap variant reuses the existing `SET_MEMORY_THRESHOLD = 128` constant introduced for `putNulls` in SPARK-57024. Below the threshold, an inline byte loop avoids the JNI fixed cost of `Unsafe.setMemory`; at or above, `setMemory` dominates. Also extends `WritableColumnVectorBulkFillBenchmark` (added in SPARK-57042 / #56084, extended for `putNulls` in SPARK-57036 / #56082) with a `putNotNulls` case mirroring the existing `putNulls` case, so this change has direct before/after numbers in the benchmark. ### Why are the changes needed? `putNotNulls` runs once per batch in the vectorized reader path (`WritableColumnVector.reset()` is called from `ColumnarBatch.close()` and via `releaseColumns()`, both of which happen at batch boundaries). The original per-byte loop is meaningfully slower than a `memset` for the typical batch capacity (4096 elements). Measured on Apple M4 Max + OpenJDK 21 via `WritableColumnVectorBulkFillBenchmark` (the `putNotNulls` case added by this PR), Rate (M elements/s): | count | OnHeap baseline | OnHeap patched | OffHeap baseline | OffHeap patched | OffHeap delta | | ------: | --------------: | -------------: | ---------------: | --------------: | ------------: | | 1 | 228 | 228 | 182 | 218 | +20% | | 8 | 1,548 | 1,524 | 647 | 1,404 | +117% (within small-count fallback) | | 64 | 11,651 | 11,155 | 990 | 3,480 | +3.5x (still below threshold; JIT noise) | | 512 | 27,060 | 26,491 | 1,055 | 12,205 | **+11.6x** | | 4,096 | 78,952 | 80,531 | 1,029 | 30,784 | **+29.9x** | | 65,536 | 241,689 | 238,644 | 1,000 | 42,647 | **+42.7x** | OnHeap is at parity across the count sweep: the C2 compiler already auto-vectorizes the original byte loop near the byte memory-bandwidth ceiling on this hardware, so `Arrays.fill` adds no measurable throughput; the change is kept for idiomatic consistency with the other byte-fill methods (`putNulls`, `putBytes`, `putBooleans`) which all use `Arrays.fill`. GHA `Run benchmarks` numbers will land on this PR shortly via the auto-commit workflow (same flow as #56072 / #56082). ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? Existing tests; no behavior change. Ran locally: - `VectorizedRleValuesReaderSuite` - `ColumnVectorSuite` - `ColumnarBatchSuite` - `ParquetIOSuite` 237 tests, all pass. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 4.7) Closes #56156 from viirya/SPARK-57111. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com> (cherry picked from commit d9bfbc5) Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com>
|
Merged to master/branch-4.x. Thanks @dongjoon-hyun |
What changes were proposed in this pull request?
Follow-up to #56072 (SPARK-57024) and #56082 (SPARK-57036).
WritableColumnVector.putNotNulls(rowId, count)clears a run of thenulls bitmap. It is called once per batch from
WritableColumnVector.reset()(whennumNulls > 0) and from theappendNotNulls()path. Both OnHeap and OffHeap implementations areper-element byte loops — the same degenerate pattern that SPARK-57024
fixed for
putNullsand SPARK-57036 fixed forputBytes/putBooleans.This change applies the same intrinsic substitutions:
OnHeapColumnVector.putNotNulls(rowId, count)Arrays.fill(byte[], ..., (byte) 0)OffHeapColumnVector.putNotNulls(rowId, count)Platform.setMemory(addr, (byte) 0, count)with small-count fallbackThe OffHeap variant reuses the existing
SET_MEMORY_THRESHOLD = 128constant introduced for
putNullsin SPARK-57024. Below the threshold,an inline byte loop avoids the JNI fixed cost of
Unsafe.setMemory; ator above,
setMemorydominates.Also extends
WritableColumnVectorBulkFillBenchmark(added inSPARK-57042 / #56084, extended for
putNullsin SPARK-57036 / #56082)with a
putNotNullscase mirroring the existingputNullscase, sothis change has direct before/after numbers in the benchmark.
Why are the changes needed?
putNotNullsruns once per batch in the vectorized reader path(
WritableColumnVector.reset()is called fromColumnarBatch.close()and via
releaseColumns(), both of which happen at batch boundaries).The original per-byte loop is meaningfully slower than a
memsetforthe typical batch capacity (4096 elements).
Measured on Apple M4 Max + OpenJDK 21 via
WritableColumnVectorBulkFillBenchmark(theputNotNullscase addedby this PR), Rate (M elements/s):
OnHeap is at parity across the count sweep: the C2 compiler already
auto-vectorizes the original byte loop near the byte memory-bandwidth
ceiling on this hardware, so
Arrays.filladds no measurablethroughput; the change is kept for idiomatic consistency with the
other byte-fill methods (
putNulls,putBytes,putBooleans) whichall use
Arrays.fill.GHA
Run benchmarksnumbers will land on this PR shortly via theauto-commit workflow (same flow as #56072 / #56082).
Does this PR introduce any user-facing change?
No.
How was this patch tested?
Existing tests; no behavior change. Ran locally:
VectorizedRleValuesReaderSuiteColumnVectorSuiteColumnarBatchSuiteParquetIOSuite237 tests, all pass.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.7)