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]

Reply via email to