[SPARK-58163][SQL] Extract the BroadcastHashJoinExec read-only relation setup into a shared helper#57294
Closed
LuciferYang wants to merge 2 commits into
Closed
Conversation
…on setup into a shared helper ### What changes were proposed in this pull request? `BroadcastHashJoinExec` takes a read-only copy of the broadcast `HashedRelation` and records its size as the task's peak execution memory in three places using identical logic: the interpreted path in `doExecute` (both the null-aware anti-join branch and the regular branch) and the generated code in `prepareBroadcast`, which re-emits the two statements into every BroadcastHashJoinExec whole-stage-codegen stage. This PR extracts that type-independent setup into a shared helper `BroadcastHashJoinExec.buildReadOnlyRelation(broadcast): HashedRelation` and calls it from all three paths. The generated relation initializer shrinks from two statements to a single static call. ### Why are the changes needed? This is part of the umbrella SPARK-56908 (reduce the size of code generated by whole-stage codegen), following the same pattern as SPARK-57909 (`ColumnarToRowExec.advanceBatch`). It also de-duplicates the two interpreted `doExecute` copies, so the paths can no longer drift. ### Does this PR introduce _any_ user-facing change? No. The helper performs the same asReadOnlyCopy + peak-memory bookkeeping in the same order as before; this is a pure code-organization change. ### How was this patch tested? Existing tests, run with whole-stage codegen both on and off: `OuterJoinSuite`, `InnerJoinSuite`, `ExistenceJoinSuite`, and `BroadcastJoinSuite`. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8)
Trim the over-long inline comment in `prepareBroadcast` and the `buildReadOnlyRelation` scaladoc to state only what is not obvious from the code. No behavior change.
Contributor
Author
LuciferYang
added a commit
that referenced
this pull request
Jul 21, 2026
…on setup into a shared helper
### What changes were proposed in this pull request?
`BroadcastHashJoinExec` takes a read-only copy of the broadcast `HashedRelation` and records its size as the task's peak execution memory in three places using identical logic:
- the interpreted path in `doExecute`, both the null-aware anti-join branch and the regular branch, and
- the generated code in `prepareBroadcast`, which re-emits the two statements into every `BroadcastHashJoinExec` whole-stage-codegen stage.
This PR extracts that type-independent setup into a shared helper `BroadcastHashJoinExec.buildReadOnlyRelation(broadcast): HashedRelation` and calls it from all three paths. The generated relation initializer shrinks from two statements to a single static call.
Before (generated code, per `BroadcastHashJoinExec` stage):
```java
relation = ((org.apache.spark.sql.execution.joins.LongHashedRelation) references[N] /* broadcast */
.value()).asReadOnlyCopy();
incPeakExecutionMemory(relation.estimatedSize());
```
After:
```java
relation = (org.apache.spark.sql.execution.joins.LongHashedRelation)
org.apache.spark.sql.execution.joins.BroadcastHashJoinExec.buildReadOnlyRelation(
references[N] /* broadcast */);
```
The helper returns the `HashedRelation` supertype, so the generated code casts the result back to the concrete relation class; the mutable-state field keeps its precise static type for the hot-path `getValue`/`get` dispatch, exactly as before.
This follows the same "extract type-independent generated machinery into a compiled helper" pattern as SPARK-57909 (`ColumnarToRowExec.advanceBatch`), and additionally de-duplicates the two interpreted `doExecute` copies.
Measured with `WholeStageCodegenSizeBenchmark` (added under SPARK-57915; plans all 135 TPC-DS queries over empty tables), current `master` vs. this change:
| metric | master | this PR | delta |
| --- | --- | --- | --- |
| constant pool, summed over stages | 432,881 | 430,503 | -0.55% |
| max method bytecode, summed over stages | 841,185 | 841,161 | ~unchanged |
| source code size (chars) | 21,846,001 | 21,846,031 | ~unchanged |
| inner classes | 258 | 258 | unchanged |
| codegen fallbacks | 0 | 0 | unchanged |
The reduction shows up in the constant pool, since the `asReadOnlyCopy` / `estimatedSize` / `incPeakExecutionMemory` method references are now emitted once instead of per stage. `max method bytecode` is unchanged because the relation initializer is not a stage's largest method, and the source-char count is flat because the fully-qualified helper call is about as long as the two inline statements it replaces -- the win is in the compiled constant pool, which is what gates the 64KB method / constant-pool limits.
### Why are the changes needed?
This is part of the umbrella SPARK-56908 (reduce the size of code generated by whole-stage codegen). The relation setup is real bytecode and constant-pool method-references that Janino cannot fold away; collapsing it to a single helper call compiles it once per JVM instead of re-emitting it into every `BroadcastHashJoinExec` stage. It also removes two copies of the interpreted-path logic, so the paths can no longer drift.
### Does this PR introduce _any_ user-facing change?
No. The helper performs the same `asReadOnlyCopy` and peak-memory bookkeeping in the same order as the previous code; this is a pure code-organization change with no behavioral difference.
### How was this patch tested?
Existing tests, run with whole-stage codegen both on and off:
- `OuterJoinSuite`, `InnerJoinSuite`, `ExistenceJoinSuite` (the join operators' correctness across build sides and codegen on/off), and
- `BroadcastJoinSuite` (broadcast build path, null-aware anti join, and metrics).
No behavior changes, so no new tests are added; the extracted setup is covered by the existing broadcast-join suites on both the interpreted and generated paths.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #57294 from LuciferYang/broadcasthashjoin-readonly-relation-helper.
Authored-by: YangJie <yangjie01@baidu.com>
Signed-off-by: yangjie01 <yangjie01@baidu.com>
(cherry picked from commit cf2a6f5)
Signed-off-by: yangjie01 <yangjie01@baidu.com>
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
BroadcastHashJoinExectakes a read-only copy of the broadcastHashedRelationand records its size as the task's peak execution memory in three places using identical logic:doExecute, both the null-aware anti-join branch and the regular branch, andprepareBroadcast, which re-emits the two statements into everyBroadcastHashJoinExecwhole-stage-codegen stage.This PR extracts that type-independent setup into a shared helper
BroadcastHashJoinExec.buildReadOnlyRelation(broadcast): HashedRelationand calls it from all three paths. The generated relation initializer shrinks from two statements to a single static call.Before (generated code, per
BroadcastHashJoinExecstage):After:
The helper returns the
HashedRelationsupertype, so the generated code casts the result back to the concrete relation class; the mutable-state field keeps its precise static type for the hot-pathgetValue/getdispatch, exactly as before.This follows the same "extract type-independent generated machinery into a compiled helper" pattern as SPARK-57909 (
ColumnarToRowExec.advanceBatch), and additionally de-duplicates the two interpreteddoExecutecopies.Measured with
WholeStageCodegenSizeBenchmark(added under SPARK-57915; plans all 135 TPC-DS queries over empty tables), currentmastervs. this change:The reduction shows up in the constant pool, since the
asReadOnlyCopy/estimatedSize/incPeakExecutionMemorymethod references are now emitted once instead of per stage.max method bytecodeis unchanged because the relation initializer is not a stage's largest method, and the source-char count is flat because the fully-qualified helper call is about as long as the two inline statements it replaces -- the win is in the compiled constant pool, which is what gates the 64KB method / constant-pool limits.Why are the changes needed?
This is part of the umbrella SPARK-56908 (reduce the size of code generated by whole-stage codegen). The relation setup is real bytecode and constant-pool method-references that Janino cannot fold away; collapsing it to a single helper call compiles it once per JVM instead of re-emitting it into every
BroadcastHashJoinExecstage. It also removes two copies of the interpreted-path logic, so the paths can no longer drift.Does this PR introduce any user-facing change?
No. The helper performs the same
asReadOnlyCopyand peak-memory bookkeeping in the same order as the previous code; this is a pure code-organization change with no behavioral difference.How was this patch tested?
Existing tests, run with whole-stage codegen both on and off:
OuterJoinSuite,InnerJoinSuite,ExistenceJoinSuite(the join operators' correctness across build sides and codegen on/off), andBroadcastJoinSuite(broadcast build path, null-aware anti join, and metrics).No behavior changes, so no new tests are added; the extracted setup is covered by the existing broadcast-join suites on both the interpreted and generated paths.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)