Max Gekk created SPARK-59765:
--------------------------------
Summary: Codegen size checks over empty TPC-DS tables measure
stubs for broadcast joins
Key: SPARK-59765
URL: https://issues.apache.org/jira/browse/SPARK-59765
Project: Spark
Issue Type: Test
Components: SQL
Affects Versions: 5.0.0
Reporter: Max Gekk
Two instruments measure the code whole-stage codegen generates for the TPC-DS
queries without running them: {{BenchmarkQueryTest.checkGeneratedCode}}, used
by {{TPCDSQuerySuite}} and its variants (see SPARK-59764), and
{{WholeStageCodegenSizeBenchmark}} (SPARK-57915), the instrument for the
SPARK-56908 umbrella. Both plan each query over empty TPC-DS tables with AQE
disabled, and call {{doCodeGen()}} on every {{WholeStageCodegenExec}}.
With AQE disabled, {{BroadcastHashJoinExec.prepareBroadcast}} executes the
broadcast while generating code. Over an empty table the relation is
{{EmptyHashedRelation}}, and {{HashJoin}} then generates a one-line comment in
place of the join: "If HashedRelation is empty, hash inner join simply returns
nothing" (and the semi join equivalent), in which case the code of every
operator after the join in the stage is not generated either. For an anti join
it generates only the pass-through. {{BroadcastNestedLoopJoinExec}} has a
similar shortcut for an empty build side. So a stage with a broadcast join is
measured as a stub, and its method sizes and constant pools are understated.
How much, on master (4cfbd411966): with {{WholeStageCodegenSizeBenchmark}}'s
setup (injected SF100 statistics with CBO, AQE off, empty tables), the
generated code of 324 of the 2201 whole-stage stages of the 129 TPC-DS v1.4 and
v2.7 queries contains the empty-relation stub, in 127 of the 129 queries.
Measured by planning each query as {{TPCDSQueryWithStatsSuite}} does, taking
{{codegenStringSeq(executedPlan)}}, and counting the stages whose source
contains "If HashedRelation is empty".
In a fork close to master, generating those stages in full made 374 stages
larger, the largest by about 3.5KB of bytecode for one method. So the
benchmark's totals and the 8000-byte check both see less code than the queries
compile over real data.
Proposed fix, for both: generate each stage's code from a copy of the stage in
which every {{BroadcastExchangeExec}} (including one under a
{{ReusedExchangeExec}}) reads a one-row {{LocalTableScanExec}} of non-null
default values. The joins then generate the code they generate for data, and
nothing else about the plan changes. Assert that no generated code still
contains the empty-relation comment, so the stub cannot come back unnoticed.
One approximation remains: a one-row build side makes every key unique, so a
join generates its unique-key form; a join whose real build side repeats keys
also has the loop over matches.
Populating the tables with data would also work, but a fixed row is filtered
out by any literal predicate on the build side (for example {{d_year = 2000}}),
which leaves the relation empty again.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]