Max Gekk created SPARK-60786:
--------------------------------
Summary: Pass integral, floating, date and timestamp literals to
generated classes by reference, so a new value does not recompile
Key: SPARK-60786
URL: https://issues.apache.org/jira/browse/SPARK-60786
Project: Spark
Issue Type: Improvement
Components: SQL
Affects Versions: 5.0.0
Reporter: Max Gekk
{{Literal.doGenCode}} writes the value of a boolean, byte, short, integer,
long, float, double, date, timestamp, time and interval literal into the
generated Java source as text (for example {{filter_value_0 =
inputadapter_value_0 == 6;}}). Every other type goes through
{{CodegenContext.addReferenceObj}}, which stores the value in the class's
{{references}} array, so the source only reads {{((UTF8String) references[k] /*
literal */)}}.
{{CodeGenerator}}'s cache is keyed by the source text, so a query that differs
only in such a literal is a new source and a new compile. A named parameter
does not help: {{BindParameters}} substitutes {{:p}} with a literal before
planning.
Measured on stock Spark 4.2.0, local mode, JDK 25, {{SELECT count(*) FROM t
WHERE i = <value>}} over 100,000 cached rows, mean of 40 queries:
* the same integer each time: about 19 ms, 0 classes compiled;
* a new integer each time: about 40 ms, 3 classes compiled (the stage under the
driver's class loader, the stage under the executor's, and the cached table's
batch filter), about 11 ms of it in Janino;
* a new string each time: 0 classes compiled.
On a cluster the driver compiles the stage once and each executor compiles the
stage and the batch filter again. A dashboard that changes a date or an integer
filter on every interaction compiles on every interaction, and a loop of
distinct values larger than {{spark.sql.codegen.cache.maxEntries}} (100
classes, about 30 such queries in local mode) recompiles every query on every
pass.
Proposal: pass these literals through {{references}} as other types already
are, reading them into a final field once (or a local before the loop), so that
queries differing only in such values share one class. A prototype stand-in
expression that does this for one integer (test code, master-based build, AMD
EPYC 7763) compiled no class for a new value and took about 20 ms, the same as
repeating one literal, against about 43 ms and 2 classes for the inlined
literal.
Open questions:
* Steady-state cost: a value read from a field is not a compile-time constant,
so the JIT cannot fold it into the comparison or strength-reduce on it (for
example a division or modulo by a literal). The prototype measured about 12 ns
a row either way over 20 million cached rows, but that is one shape and one
run; a few shapes where folding matters ({{x / <lit>}}, {{x % <lit>}}, {{x &
<mask>}}) need measuring.
* Whether to apply it everywhere, or only where it pays: optimizations that
read a literal's value at generation time (for example skipping a
divide-by-zero check when the divisor is a non-zero literal, SPARK-57198) must
keep seeing the value, so the change is to how the value reaches the bytecode,
not to what the generator knows about it.
* {{ConstantFolding}} and other plan-time uses are unaffected; only generated
code changes.
Related: SPARK-60048 (whole-stage codegen's split decision keyed by a digest
that ignores literal values, since {{current_timestamp()}} and similar literals
change it on every run), SPARK-27871 (generated variable names that changed on
every run and defeated the same cache, fixed), SPARK-44795 (the cache keyed by
class loader as well as source).
Write-up with the measurements and scripts:
https://vecbricks.github.io/how-spark-compiles-a-query/
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]