Skip to content

[SPARK-57111][SQL] Use intrinsic bulk-fill APIs for putNotNulls#56156

Closed
viirya wants to merge 4 commits into
apache:masterfrom
viirya:SPARK-57111
Closed

[SPARK-57111][SQL] Use intrinsic bulk-fill APIs for putNotNulls#56156
viirya wants to merge 4 commits into
apache:masterfrom
viirya:SPARK-57111

Conversation

@viirya

@viirya viirya commented May 27, 2026

Copy link
Copy Markdown
Member

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)

@dongjoon-hyun dongjoon-hyun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, LGTM.

@dongjoon-hyun

Copy link
Copy Markdown
Member

@viirya

viirya commented May 27, 2026

Copy link
Copy Markdown
Member Author

#56159 is for baseline numbers. This PR adds a more case to the benchmark so no baseline.

dongjoon-hyun pushed a commit that referenced this pull request May 28, 2026
…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>
dongjoon-hyun pushed a commit that referenced this pull request May 28, 2026
…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>
viirya and others added 4 commits May 27, 2026 22:30
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
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, 0.5X -> 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.5X -> 15.7X.

@viirya

viirya commented May 28, 2026

Copy link
Copy Markdown
Member Author

@dongjoon-hyun Thanks for going through the numbers. Quick triage:

Lines 290 / 297putNotNulls count=4096 / 65536 going 0.5X -> 11.9X / 15.7X: those are the real wins this PR is going for (the OffHeap setMemory path kicks in past the 128-element threshold).

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:

  • Line 24, putBooleans count=64: this PR doesn't touch putBooleans at all (that one was fixed in SPARK-57036 and is already on master). Both the baseline and the patched results execute exactly the same code, yet the numbers move by ~40% across the two GHA runs.

  • Line 276, putNotNulls count=64: 64 is below the SET_MEMORY_THRESHOLD = 128, so the patched path is the inline byte-loop fallback — i.e. structurally the same per-byte loop as master's baseline. Both runs execute the same byte loop here.

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):

count putBooleans baseline → patched putBytes baseline → patched putNulls baseline → patched putNotNulls baseline → patched
8 1633 → 1222 (−25%) 849 → 996 (+17%) 478 → 305 (−36%) 534 → 303 (−43%)
64 2550 → 1515 (−41%) 1331 → 2531 (+90%) 1733 → 463 (−73%) 1162 → 465 (−60%)
512 19325 → 20244 (+5%) 18704 → 19733 (+5%) 13000 → 14688 (+13%) 1371 → 14186 (+10x)
4096 39185 → 40351 (+3%) 39244 → 40089 (+2%) 34880 → 36785 (+5%) 1407 → 36768 (+26x)

putBooleans / putBytes / putNulls are unchanged by this PR — but small-count cells move by ±25–90% between the two GHA runs. putNotNulls shows the same noise floor at small counts and then a clean break above the threshold where the actual code change applies.

putBytes count=64 going from 1331 → 2531 in the same run pair is, I think, the cleanest counter-example: same code, GHA paired the two runs onto different runner state, and the rate doubled.

So: lines 290 / 297 are real, lines 24 / 276 (and the −25/−36/−43% at count=8) are run-to-run jitter and not anything this PR introduced. Happy to re-trigger the JDK 21 benchmark a couple more times if it would help triangulate, but I don't think the underlying picture will change.

@dongjoon-hyun

dongjoon-hyun commented May 28, 2026

Copy link
Copy Markdown
Member

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.

@viirya

viirya commented May 28, 2026

Copy link
Copy Markdown
Member Author

Thank you @dongjoon-hyun.

@viirya

viirya commented May 28, 2026

Copy link
Copy Markdown
Member Author

Note that the failed test org.apache.spark.sql.jdbc.v2.OracleIntegrationSuite is a known flaky one and not related to this change.

@viirya viirya closed this in d9bfbc5 May 28, 2026
viirya added a commit that referenced this pull request May 28, 2026
### 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>
@viirya
viirya deleted the SPARK-57111 branch May 28, 2026 20:01
@viirya

viirya commented May 28, 2026

Copy link
Copy Markdown
Member Author

Merged to master/branch-4.x.

Thanks @dongjoon-hyun

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants