cloud-fan commented on code in PR #58045:
URL: https://github.com/apache/spark/pull/58045#discussion_r3966357431
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/With.scala:
##########
@@ -162,7 +354,85 @@ case class CommonExpressionDef(child: Expression, id:
CommonExpressionId = new C
* referenced, so that we can determine the data type and nullable of the
reference node.
*/
case class CommonExpressionRef(id: CommonExpressionId, dataType: DataType,
nullable: Boolean)
- extends LeafExpression with Unevaluable {
+ extends LeafExpression {
def this(exprDef: CommonExpressionDef) = this(exprDef.id, exprDef.dataType,
exprDef.nullable)
+
+ /**
+ * The definition this reference names, and the cell holding its value for
the current row. Both
+ * are wired by the enclosing [[With]] before it evaluates its child, and
are left out of the case
+ * class parameters so that equality and canonicalization are unchanged --
and so that a rule
+ * comparing two references does not compare their cells.
+ *
+ * `@transient` for the reason [[CommonExpressionCell]]'s own fields are: a
binding lives only for
+ * the duration of one `With.eval`, which restores it on the way out, so
serializing a task sees
+ * null. A serialization that happens while an evaluation is still on the
stack would otherwise
+ * capture a live binding, and the deserialized reference would evaluate
that definition rather
+ * than raise, dragging the definition's whole subtree along with it.
+ */
+ @transient private var definition: Expression = _
+ @transient private var cell: CommonExpressionCell = _
+
+ private[expressions] def bindTo(exprDef: CommonExpressionDef): Unit = {
+ definition = exprDef.child
+ cell = exprDef.cell
+ }
+
+ private[expressions] def boundDefinition: Expression = definition
+ private[expressions] def boundCell: CommonExpressionCell = cell
+
+ private[expressions] def bindTo(
+ newDefinition: Expression,
+ newCell: CommonExpressionCell): Unit = {
+ definition = newDefinition
+ cell = newCell
+ }
+
override val nodePatterns: Seq[TreePattern] = Seq(COMMON_EXPR_REF)
+
+ // The cell is cleared by the enclosing `With` on every entry, so this reads
mutable state.
+ override def stateful: Boolean = true
+
+ /**
+ * A copy must not carry this reference's binding: the copy belongs to a
different `With`, which
+ * wires it to its own cell. `LeafLike` returns `this` here, which would
hand two `With`s one
+ * reference object and let whichever wires last decide what both of them
read --
+ * `NamedLambdaVariable` overrides this for the same reason.
+ */
+ override def withNewChildrenInternal(
+ newChildren: IndexedSeq[Expression]): CommonExpressionRef = copy()
+
+ override def eval(input: InternalRow): Any = {
+ if (cell == null) {
+ throw SparkException.internalError(
+ s"Cannot evaluate a common expression reference outside its With:
$this")
+ }
+ cell.get(definition, input)
+ }
+
+ /**
+ * Computes the definition into the shared slots if this row has not done so
yet, then reads them.
+ * The code that computes it is emitted here rather than by the enclosing
`With`, so it runs where
+ * the first reference is reached -- behind a short-circuiting operator or a
nested conditional,
+ * if that is where the reference sits.
+ *
+ * A second reference emits the same code again, which never runs because
the flag is set. What
+ * that code is depends on the definition: a call, where the definition can
be put in a method, so
+ * that a definition that is or holds another `With` is not pasted once per
reference at every
+ * level; the body itself otherwise, whose locals are declared inside each
guard. No copy of one
+ * body encloses another -- for that, a definition would have to reference
its own id, directly or
Review Comment:
**Nit (P3):** An acyclic sibling dependency can enclose another fill body
without recursing: `withCommonExprs` registers every sibling slot before
generating the child, so `d2 = Add(ref(d1), 1)` resolves `d1` and completes
normally. Only self-reference or a dependency cycle re-enters a slot already
being filled. Could we state that distinction here and narrow the earlier
`refsToBind` claim that codegen refuses every definition-reference shape?
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -207,6 +207,163 @@ class CodegenContext extends Logging {
throw QueryExecutionErrors.lambdaVariableNotDefinedError(id))
}
+ /**
+ * The slots a `CommonExpressionRef` reads: the value and its nullness, plus
the flag saying
+ * whether this row has computed them yet, and the definition to compute
them from.
+ */
+ case class CommonExprSlots(
+ value: ExprCode,
+ computed: String,
+ definition: Expression) {
+
+ // Not constructor parameters: a mutable one takes part in
`equals`/`hashCode`/`copy`, so a
+ // slot's hash would change as it is filled and a `copy` would carry
another scope's code.
+ private var fillCode: Option[Block] = None
+ private var filling: Boolean = false
+
+ /**
+ * The code that computes the definition into the slots and sets
`computed`, which a reference
+ * emits behind that flag. Cached, so every reference shares whatever
mutable state the
+ * definition allocated, such as an RNG.
+ *
+ * The cache lives on the slot, so it lasts exactly as long as the scope:
the code names the
+ * `INPUT_ROW` and `currentVars` in effect when it was generated.
`GenerateOrdering` generates
+ * its key once per comparison side under a different row variable, so a
slot shared between the
+ * sides would read the wrong row, or not compile where
`Expression.reduceCodeSize` has hoisted
+ * the reference into a method taking one row.
+ *
+ * `filling` catches a definition that references its own id, which would
otherwise re-enter and
+ * recurse, since `fillCode` is set only after `definition.genCode`
returns.
+ *
+ * The body goes into a method where it can and is worth it -- a
definition that is or holds
+ * another `With`, or a body past the split threshold -- so it is emitted
once per scope rather
+ * than once per reference, which for nested `With`s would double per
level. A method is only
+ * possible where the definition reads the input row rather than local
variables, the condition
+ * `reduceCodeSize` splits under. That is not the same as whole-stage
codegen being off: a
+ * whole-stage `Project` or `Filter` passes local variables, while
+ * `SortMergeJoinExec.createJoinKey` and the aggregate output paths
generate against a row.
+ */
+ def fill: Block = {
+ if (fillCode.isEmpty) {
+ if (filling) {
+ throw SparkException.internalError(
+ "Cannot generate a common expression whose definition references
it: " +
+ definition.toString)
+ }
+ filling = true
+ try {
+ fillCode = Some(build)
+ } finally {
+ filling = false
+ }
+ }
+ fillCode.get
+ }
+
+ private def build: Block = {
+ val defGen = definition.genCode(CodegenContext.this)
+ // Whether the isNull slot exists is decided by the definition, so it is
read off the slot
+ // rather than off a reference's own `nullable`: taking it from both
would let the two
+ // disagree, and either emit `false = <isNull>;`, which does not
compile, or leave the slot
+ // holding the previous row's nullness.
+ val assignIsNull = if (value.isNull == FalseLiteral) {
+ ""
+ } else {
+ s"${value.isNull} = ${defGen.isNull};"
+ }
+ val body = code"""
+ |${defGen.code}
+ |$assignIsNull
+ |${value.value} = ${defGen.value};
+ |$computed = true;
+ """.stripMargin
+ // TODO(SPARK-59295): cover the local-variable case too, by passing the
`currentVars` values a
+ // definition reads into the method as parameters, the way
+ // `subexpressionEliminationForWholeStageCodegen` does. It needs a
decision first:
+ // `getLocalInputVariableValues` hoists an input variable that is not
evaluated yet to
+ // before the call, which for a reference behind a branch means
evaluating it on rows that
+ // never reach the reference.
+ val canPutInMethod = INPUT_ROW != null && currentVars == null
+ // A definition that is or holds another `With` is the shape whose code
doubles per level,
+ // and what this is aimed at. It is not the only one -- a definition
referencing a sibling
+ // definition of the same `With` doubles the same way, and codegen
accepts that, since the
+ // sibling's slots are in scope while this definition is generated
(`With.refsToBind` says
+ // why nothing builds that tree, and that evaluating one raises). What
bounds those is not
+ // the length arm below: `body` is assembled after `definition.genCode`
already ran
+ // `reduceCodeSize`, so the arm fires only in the band just under the
threshold. It is
+ // `reduceCodeSize` itself, which hoists whichever node's code first
passes the threshold as
+ // generation walks up, capping what one level contributes, so the code
stays linear in the
+ // depth either way. The length arm just keeps the same body from being
split once per
+ // reference, which leaves the methods small and the code as large.
+ val worthAMethod = definition.exists(_.isInstanceOf[With]) ||
Review Comment:
**Nit (P3):** `With` already sets `WITH_EXPRESSION` in `nodePatterns`, and
those bits include descendants, so this recursive `exists` walk repeats work
for every definition. Could we import the pattern and use
`definition.containsPattern(WITH_EXPRESSION)` here? It preserves the same
root-or-descendant test without revisiting the expression subtree.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/With.scala:
##########
@@ -38,11 +70,154 @@ case class With(child: Expression, defs:
Seq[CommonExpressionDef])
override def dataType: DataType = child.dataType
override def nullable: Boolean = child.nullable
override def children: Seq[Expression] = child +: defs
+
+ /**
+ * The references in `child` that name one of these definitions, paired with
the definition each
+ * names, found once since the tree does not change between evaluations.
Only `child` is scanned,
+ * which is why the checks below refuse a definition holding a reference of
this scope.
+ */
+ @transient private lazy val refsToBind: IndexedSeq[(CommonExpressionRef,
CommonExpressionDef)] = {
+ // Three shapes whose bindings cannot be kept straight, each of which this
path used to answer
Review Comment:
**Nit (P3):** `this path used to answer for with` is not grammatical, so the
outcome of these three shapes is hard to follow. Could this say that the path
previously evaluated them incorrectly or overflowed the stack?
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/WithExpressionEvalSuite.scala:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.catalyst.expressions
+
+import org.apache.spark.{SparkException, SparkFunSuite}
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext,
CodegenFallback, ExprCode, GenerateMutableProjection}
+import org.apache.spark.sql.catalyst.plans.SQLHelper
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{BooleanType, DataType, IntegerType}
+
+/**
+ * Evaluation of [[With]] and the memoization it gives a
[[CommonExpressionRef]]. The rewrite that
+ * decides which `With`s reach evaluation at all is covered by
`RewriteWithExpressionSuite`.
+ */
+class WithExpressionEvalSuite extends SparkFunSuite with SQLHelper {
+
+ /**
+ * A stand-in for a stateful generator: every evaluation returns the next
integer, so a second
+ * evaluation within one row is directly observable. Only used on the
interpreted path.
+ */
+ private case class Counter() extends LeafExpression with Nondeterministic {
+ @transient private var n = 0
+ override def stateful: Boolean = true
+ override def dataType: DataType = IntegerType
+ override def nullable: Boolean = false
+ override protected def initializeInternal(partitionIndex: Int): Unit = {}
+ override protected def evalInternal(input: InternalRow): Any = { n += 1; n
}
+ override protected def doGenCode(ctx: CodegenContext, ev: ExprCode):
ExprCode =
+ throw new UnsupportedOperationException
+ }
+
+ private def counter(): Counter = {
+ val c = Counter()
+ c.initialize(0)
+ c
+ }
+
+ /**
+ * A node that has to be evaluated interpretively even when its parent is
generated, so that a
+ * reference below it takes the interpreted path out of generated code.
+ */
+ private case class Fallback(child: Expression) extends UnaryExpression with
CodegenFallback {
+ override def dataType: DataType = child.dataType
+ override def eval(input: InternalRow): Any = child.eval(input)
+ override protected def withNewChildInternal(newChild: Expression):
Fallback =
+ copy(child = newChild)
+ }
+
+ test("a definition is evaluated once per row however many references read
it") {
+ // `ref + ref` is the shape `BETWEEN` produces. Memoized, both references
read one value, so the
+ // sum is 2n on the nth row; inlined it would be n + (n + 1).
+ val w = With(counter()) { case Seq(ref) => Add(ref, ref) }
+ assert((1 to 4).map(_ => w.eval(InternalRow.empty)) == Seq(2, 4, 6, 8))
+ }
+
+ test("a definition is not evaluated on a row that reaches no reference") {
+ val c = counter()
+ // The branch is inside the `With`, so the `With` is entered on every row
and does clear its
+ // cells. What it must not do is evaluate the definition before a
reference is reached; putting
+ // the branch outside would only test that `If` does not evaluate the arm
it did not take.
+ val w = With(c) { case Seq(ref) => If(Literal.TrueLiteral, Literal(-1),
Add(ref, ref)) }
+ // -1 whatever the definition does, since the taken arm is a literal: the
assertion below is the
+ // load-bearing one, and this only says the evaluation ran.
+ assert((1 to 3).map(_ => w.eval(InternalRow.empty)) == Seq(-1, -1, -1))
+ // The counter is still at 0, so the first row that does reach a reference
sees 1. Eager filling
+ // would have consumed three values and this would read 8.
+ assert(With(c) { case Seq(ref) => Add(ref, ref) }.eval(InternalRow.empty)
== 2)
+ }
+
+ test("a reference behind a short-circuiting operator is not read when the
left side is false") {
+ // Neither pre-evaluating the definition into a project nor guarding that
column by the branch
+ // can express this: both evaluate on every row the branch is reached on,
while `And`
+ // short-circuits before the reference. The `And` is inside the `With` so
that the `With` is
+ // entered and only the reference is skipped.
+ val c = counter()
+ val w = With(c) { case Seq(ref) =>
+ And(Literal.FalseLiteral, GreaterThanOrEqual(ref, Literal(1)))
+ }
+ // False whatever the definition does, as in the case above: the counter
read is what bites.
+ // This is not that case repeated -- there the arm is skipped by `If`,
here by `And`'s short
+ // circuit, and the two are separate paths in both evaluation and codegen.
+ assert((1 to 3).map(_ => w.eval(InternalRow.empty)) == Seq(false, false,
false))
+ assert(With(c) { case Seq(ref) => Add(ref, ref) }.eval(InternalRow.empty)
== 2)
+ }
+
+ test("a nested With memoizes each scope separately") {
+ val outer = counter()
+ val inner = counter()
+ // The outer reference is read twice, and one of its uses wraps an inner
`With` whose own
+ // reference is also read twice, so each row is `o + (2i + o)` with `o`
and `i` both n.
+ val w = With(outer) { case Seq(o) =>
+ Add(o, With(inner) { case Seq(i) => Add(Add(i, i), o) })
+ }
+ assert((1 to 3).map(_ => w.eval(InternalRow.empty)) == Seq(4, 8, 12))
+ }
+
+ test("a With rebuilt by a rule still memoizes") {
+ val w = With(counter()) { case Seq(ref) => Add(ref, ref) }
+ // Evaluate the original first. A rule rebuilds the definition, and
therefore its cell, while
+ // handing the new `With` the original's reference objects, so an
implementation that bound the
+ // references once would leave the rebuilt `With` reading this
evaluation's cell.
+ assert(w.eval(InternalRow.empty) == 2)
+ // The rule has to hand back a node that is not `==` the one it replaces,
or `transformUp` keeps
+ // the original -- `Counter()` is a case class with no parameters, so a
fresh one compares equal
+ // to the old one. Wrapping it changes the tree while leaving the
definition's value sequence,
+ // its data type and its nullability alone, so the references are carried
over rather than
+ // rebuilt, which is the shape that needs the per-evaluation rebinding.
+ val rewritten = w.transformUp { case _: Counter => Add(counter(),
Literal(0)) }
+ .asInstanceOf[With]
+ assert(rewritten ne w, "the rule has to have rebuilt something")
+ assert((1 to 3).map(_ => rewritten.eval(InternalRow.empty)) == Seq(2, 4,
6))
+ }
+
+ test("SPARK-58902: a copy of a With owns its references and its cells") {
+ // A deliberately non-stateful definition, which is what makes
`CommonExpressionDef.stateful`
+ // load-bearing: `mapChildren` finds nothing changed below it, so without
the override the copy
+ // gets the original definition object, cell included. (A `Rand`
definition would hide that.)
+ // Likewise `CommonExpressionRef.stateful`, since
`LeafLike.withNewChildrenInternal` returns
+ // `this` and the two `With`s would otherwise share references while
owning two cells.
+ val w1 = With(Literal(1)) { case Seq(ref) => Add(ref, ref) }
+ val w2 = w1.freshCopyIfContainsStatefulExpression().asInstanceOf[With]
+ def refOf(e: Expression): CommonExpressionRef =
+ e.collect { case r: CommonExpressionRef => r }.head
+ assert(w1 ne w2, "a stateful With has to be copied")
+ assert(refOf(w1.child) ne refOf(w2.child), "the copy has to own its
references")
+ assert(w1.defs.head.cell ne w2.defs.head.cell, "the copy has to own its
cell")
+ }
+
+ test("SPARK-58902: two Withs over one set of references each read their own
definition") {
+ // The shape a rule produces when it rewrites only the definition: the new
`With` is built over
+ // the original's references, since a rebuilt reference compares equal to
the one it replaces.
+ // Visibly different definitions show which cell the references actually
read.
+ val w1 = With(counter()) { case Seq(ref) => Add(ref, ref) }
+ val w2 = w1.withNewChildren(
+ IndexedSeq(w1.child, CommonExpressionDef(Literal(100),
w1.defs.head.id))).asInstanceOf[With]
+
+ assert(w1.eval(InternalRow.empty) == 2, "w1 reads its own counter")
+ assert(w2.eval(InternalRow.empty) == 200, "w2 reads its own literal")
+ assert(w1.eval(InternalRow.empty) == 4, "w1 read the value w2 memoized")
+ }
+
+ test("SPARK-58902: a definition's code is generated once however many
references read it") {
+ // Short and holding no `With`, so the body is emitted inline at each
reference. It has to be
+ // the same text, generated once: otherwise each copy calls
`addMutableState` again and the
+ // definition owns one counter per reference, which for references in
mutually exclusive
+ // positions hands out one value on two rows.
+ val ctx = new CodegenContext
+ With(MonotonicallyIncreasingID()) { case Seq(ref) => Add(ref, ref)
}.genCode(ctx)
+ val counters = ctx.inlinedMutableStates.count { case (_, name) =>
name.startsWith("count") }
+ assert(counters == 1, s"the definition was generated $counters times")
+ }
+
+ test("SPARK-58902: nesting does not multiply a definition's body when it can
go in a method") {
+ // Each level reads its definition twice, so pasting the body doubles it
per level;
+ // `CommonExprSlots.fill` putting it in a method makes it 2 at any depth.
Not covered here: the
+ // shape where the input arrives as local variables, as a whole-stage
`Project` or `Filter`
+ // passes it, where no method is possible and the count is 2, 4, 8, ...
256 at depths 1 to 8 --
+ // see SPARK-59295. A bare `CodegenContext` has `INPUT_ROW` set and
`currentVars` null.
+ val marker = 1234567
+ def nested(depth: Int): Expression = {
+ val leaf: Expression = Add(BoundReference(0, IntegerType, nullable =
false), Literal(marker))
+ (1 to depth).foldLeft(leaf) { (inner, _) =>
+ With(inner) { case Seq(ref) => Add(ref, ref) }
+ }
+ }
+ def markerCount(depth: Int): Int = {
+ val ctx = new CodegenContext
+ val source = nested(depth).genCode(ctx).code.toString +
ctx.declareAddedFunctions()
+ marker.toString.r.findAllMatchIn(source).size
+ }
+ // Pin the threshold: below the innermost body's length the fill would go
into a method of its
+ // own and the count would be 1, for a reason unrelated to nesting.
+ withSQLConf(SQLConf.CODEGEN_METHOD_SPLIT_THRESHOLD.key -> "1024") {
+ (1 to 6).foreach { depth =>
+ val count = markerCount(depth)
+ assert(count == 2, s"the innermost body was emitted $count times at
depth $depth")
+ }
+ // The 2 is the innermost body pasted at its two references, at every
depth; what stops the
+ // doubling from depth 2 on is that each enclosing definition is itself
a `With` and so goes
+ // into a method, whose call sites carry no marker.
+ //
+ // The values double per level, which a deterministic leaf does whether
a reference reads a
+ // slot or recomputes -- an arithmetic and compile check, not a second
reading of the count.
+ // Four levels keeps the product inside Int, which ANSI `Add` would
raise on past depth 10.
+ val proj = GenerateMutableProjection.generate(Seq(nested(4)))
+ assert(proj(InternalRow(1)).getInt(0) == (1 + marker) * 16)
+ }
+ }
+
+ test("SPARK-58902: a nested With sharing a reference object restores the
outer binding") {
+ // Two `With`s over one reference object, the inner one nested in the
outer one's child and
+ // redefining the id the outer one defines. Only a caller building the
case class directly
+ // produces this -- `withNewChildrenInternal` shares references between a
`With` and its
+ // replacement, which are siblings, and no rule nests a redefinition of a
live id -- so this is
+ // hardening rather than a reachable wrong answer. Binding on entry alone
is not enough: the
+ // inner `With` rebinds the shared reference to its own definition, so a
read after the inner
+ // one returned would answer with the inner definition unless the outer
binding is put back.
+ val id = new CommonExpressionId()
+ val outerDef = CommonExpressionDef(counter(), id)
+ val innerDef = CommonExpressionDef(Literal(10), id)
+ val ref = new CommonExpressionRef(outerDef)
+ val inner = With(Add(ref, ref), Seq(innerDef))
+ val outer = With(Add(Add(ref, inner), ref), Seq(outerDef))
+ // The outer definition counts, the inner one does not, so row n is n +
(10 + 10) + n. A counter
+ // is what makes the repetition worth something: leaving the inner binding
in place gives
+ // n + 20 + 10, and losing memoization of the outer definition gives n +
20 + (n + 1).
+ assert((1 to 3).map(_ => outer.eval(InternalRow.empty)) == Seq(22, 24, 26))
+
+ // The generated path never has to answer this: one id in two nested
scopes is refused while
+ // generating, so it cannot quietly disagree with the values above.
+ val literalOuter = CommonExpressionDef(Literal(1), new
CommonExpressionId())
+ val literalRef = new CommonExpressionRef(literalOuter)
+ val nested = With(
+ Add(literalRef, With(Add(literalRef, literalRef),
+ Seq(CommonExpressionDef(Literal(10), literalOuter.id)))),
+ Seq(literalOuter))
+ val generated =
intercept[SparkException](GenerateMutableProjection.generate(Seq(nested)))
+ assert(generated.getMessage.contains("is already being generated"))
+ }
+
+ test("SPARK-58902: a definition that references its own id fails without
recursing") {
+ // Only a caller building the case class directly can produce this, and it
is caught rather than
+ // left to recurse: generating the definition re-enters the same slot, and
evaluating it would
Review Comment:
**Nit (P3):** The generated half does re-enter the same slot, but the
interpreted half of this first construction uses two distinct reference
objects. Without the new validation, `refsToBind` would bind only the child
reference and the reference inside `selfDef` would fail as unbound; only the
shared-reference variant below re-enters one cell. Could we split those two
failure mechanisms in this comment?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]