MaxGekk commented on code in PR #59225:
URL: https://github.com/apache/spark/pull/59225#discussion_r4201484817


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/WholeStageCodegenExec.scala:
##########
@@ -610,6 +616,90 @@ object WholeStageCodegenExec {
   // Reset generation time of Java source code.
   // Visible for testing
   def resetCodeGenTime(): Unit = _codeGenTime.set(0L)
+
+  /**
+   * A trial compile's outcome for `WholeStageCodegenExec.chooseCode`: the 
bytecode size of each
+   * method, None where they are unknown, or the failure; and the reports and 
metric updates the
+   * compile held back for the code kept.
+   */
+  private[execution] case class TrialResult(
+      methodSizes: Try[Option[Map[String, Int]]],
+      report: () => Unit)
+
+  /**
+   * Compiles `code` as a trial (`CodeCompiler.trial`). A hit in the compile 
cache produces no
+   * method sizes, so the class is then dropped and compiled again. A hit 
happens where the class
+   * is cached while the decision for its code is not: the decision was 
evicted, or the same body
+   * was compiled earlier without being decided - with the split off, whose 
code is the recorded
+   * code without its marks, or under another limit. Should another thread 
load the class again in
+   * between, the second compile is a hit as well, and the sizes are unknown.
+   */
+  private[execution] def trialCompile(
+      code: CodeAndComment,
+      failureExpected: Boolean): TrialResult = {
+    def once(): (Try[Map[String, Int]], CodeCompiler.TrialCompile) = {
+      val (result, trial) = 
CodeCompiler.trial(failureExpected)(CodeGenerator.compile(code))
+      (result.map(_ => trial.methodSizes), trial)
+    }
+    val (sizes, trial) = once() match {
+      case (Success(m), _) if m.isEmpty =>
+        CodeGenerator.invalidateCompiled(code)
+        once()
+      case result => result
+    }
+    TrialResult(sizes.map(m => Option.when(m.nonEmpty)(m)), () => 
trial.report())
+  }
+
+  /** The methods a class runs once, whose being interpreted costs nothing per 
row. */
+  private val RunOnce = Set("<init>", "<clinit>", "init")

Review Comment:
   Done. `CodeCompiler.runOnceMethods` is `private[sql]`, and 
`WholeStageCodegenExec` uses it in place of `RunOnce`. 
`CodeGenerator.compileCacheScope(code)` returns the class loader and backend 
part of the cache key, which both `cacheKey` and `decisionKey` build from. I 
left `init_N_M` as it is in both, as you noted it's harmless today.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1271,49 +1759,159 @@ class CodegenContext extends Logging {
     DEFAULT_JVM_HUGE_METHOD_LIMIT / 2 / (MAX_BYTES_PER_SPLIT_CALL + 2 * 
arguments.length))
 
   /**
-   * The calls to the functions `splitExpressions` split out, grouped into 
functions of their own
-   * where there are too many for the calling method to be JIT-compiled, level 
by level.
+   * The calls to the functions `splitExpressions` split out, grouped by 
`groupSplitCalls` where
+   * there are too many for the calling method to be JIT-compiled. Every call 
takes `arguments`, so
+   * the groups are of `SPLIT_CALLS_PER_GROUP` calls, and the calls stay in 
the calling method while
+   * they fit in `splitCallsPerMethod`.
+   */
+  private def groupFunctionCalls(
+      calls: Seq[String],
+      funcName: String,
+      arguments: Seq[(String, String)],
+      returnType: String,
+      makeSplitFunction: String => String,
+      foldFunctions: Seq[String] => String): Seq[String] = {
+    groupSplitCalls(calls.map(SplitCall(_, arguments)), funcName, returnType, 
makeSplitFunction,
+      foldFunctions, callsLeft = splitCallsPerMethod(arguments)).map(_.call)
+  }
+
+  /** A call to a split function, and the (type, name) arguments it passes. */
+  private case class SplitCall(call: String, arguments: Seq[(String, String)])
+
+  /**
+   * Adds the function `name`, taking `arguments` and running `body`, and 
returns a call to it.
+   * Where `audited`, its arguments are kept under testing for 
`assertSplitCallsWithin`: those of a
+   * whole stage split, computed from what its code reads where it was 
generated, which an operator
+   * moving that code into a method of its own must take. The other splits' 
arguments are their
+   * callers' own, which may be locals of the method they are made in.
+   */
+  private def splitCall(
+      name: String,
+      arguments: Seq[(String, String)],
+      returnType: String,
+      body: String,
+      audited: Boolean = false): SplitCall = {
+    if (Utils.isTesting) {
+      // Passing global variables to the split method is dangerous, as any 
mutating to it is
+      // ignored and may lead to unexpected behavior.
+      arguments.foreach { case (_, argument) =>
+        assert(!mutableStateNames.contains(argument),
+          s"split function argument $argument cannot be a global variable.")
+      }
+    }
+    val argDefinitionString = arguments.map { case (t, argument) => s"$t 
$argument" }.mkString(", ")
+    val code =
+      s"""
+         |private $returnType $name($argDefinitionString) {
+         |  $body
+         |}
+           """.stripMargin
+    val call = s"${addNewFunction(name, 
code)}(${arguments.map(_._2).mkString(", ")})"
+    if (INPUT_ROW != null && arguments.exists(_._2 == INPUT_ROW)) {
+      functionsTakingRow += name
+    }
+    if (audited && Utils.isTesting) {
+      splitCallArguments(name) = arguments.map(_._2)
+    }
+    SplitCall(call, arguments)
+  }
+
+  /**
+   * The calls to split functions, grouped into functions of their own, level 
by level, until at
+   * most `callsLeft` remain in the calling method.
    *
    * The calls are what is left in the calling method, one per block of code, 
and a `CASE WHEN`
    * of a thousand branches has hundreds of blocks: their calls alone take the 
caller past the
    * 8000 bytes HotSpot compiles 
(`CodeGenerator.DEFAULT_JVM_HUGE_METHOD_LIMIT`), so the method
    * every row goes through runs interpreted. A call and its fold compile to 
at most about 20 bytes
    * plus 2 for each argument - the invocation, a load per argument, and the 
fold's store, test
-   * and branch - so the calls are left as they are while they fit in half of 
that limit, the rest
-   * being the caller's own code, and grouped otherwise. The groups are small, 
of
-   * `CodeGenerator.SPLIT_CALLS_PER_GROUP` calls, so that C2 can inline the 
split functions into
-   * each group within its inlining budget; a group of hundreds of calls stays 
compilable but has
-   * most of its calls left out of inlining, and runs slower. A group is 
folded with
-   * `foldFunctions` and wrapped with `makeSplitFunction`, which is what
-   * `generateInnerClassesFunctionCalls` does with the functions of one inner 
class, so every
-   * caller of `splitExpressions` supports it.
-   */
-  private def groupFunctionCalls(
-      calls: Seq[String],
+   * and branch - so outside whole stage codegen the calls are left as they 
are while they fit in
+   * half of that limit, the rest being the caller's own code 
(`splitCallsPerMethod`). Under whole
+   * stage codegen the caller is the operator's method, which holds every 
expression's calls, so
+   * at most `SPLIT_CALLS_PER_GROUP` are left there for each.
+   *
+   * The groups are small, of `CodeGenerator.SPLIT_CALLS_PER_GROUP` calls, so 
that C2 can inline
+   * the split functions into each group within its inlining budget; a group 
of hundreds of calls
+   * stays compilable but has most of its calls left out of inlining, and runs 
slower. A group
+   * takes the union of what its calls take, and is closed when one more call 
would take it past
+   * those calls or past the parameters a method can have; where no group can 
hold two calls, the
+   * calls are left as they are. A group is folded with `foldFunctions` and 
wrapped with
+   * `makeSplitFunction`, which is what `generateInnerClassesFunctionCalls` 
does with the functions
+   * of one inner class, so every caller of `splitExpressions` supports it.
+   */
+  private def groupSplitCalls(
+      calls: Seq[SplitCall],
       funcName: String,
-      arguments: Seq[(String, String)],
       returnType: String,
       makeSplitFunction: String => String,
       foldFunctions: Seq[String] => String,
-      level: Int = 0): Seq[String] = {
-    val callsPerMethod = splitCallsPerMethod(arguments)
-    if (calls.length <= callsPerMethod) {
+      callsLeft: Int,
+      level: Int = 0,
+      audited: Boolean = false): Seq[SplitCall] = {
+    if (calls.length <= callsLeft) {
       calls
     } else {
-      val argDefinitionString = arguments.map { case (t, name) => s"$t $name" 
}.mkString(", ")
-      val argInvocationString = arguments.map(_._2).mkString(", ")
-      val groupCalls = calls.grouped(SPLIT_CALLS_PER_GROUP).zipWithIndex.map { 
case (group, i) =>
-        val name = s"${funcName}_group${level}_$i"
-        val code =
-          s"""
-             |private $returnType $name($argDefinitionString) {
-             |  ${makeSplitFunction(foldFunctions(group))}
-             |}
-           """.stripMargin
-        s"${addNewFunction(name, code)}($argInvocationString)"
-      }.toSeq
-      groupFunctionCalls(groupCalls, funcName, arguments, returnType, 
makeSplitFunction,
-        foldFunctions, level + 1)
+      val groups = mutable.ArrayBuffer.empty[mutable.ArrayBuffer[SplitCall]]
+      var taken = Seq.empty[(String, String)]
+      calls.foreach { c =>
+        val union = (taken ++ c.arguments).distinctBy(_._2)
+        if (groups.isEmpty || groups.last.length == SPLIT_CALLS_PER_GROUP ||
+            !isValidParamLength(paramSlots(union))) {
+          groups += mutable.ArrayBuffer(c)
+          taken = c.arguments
+        } else {
+          groups.last += c
+          taken = union
+        }
+      }
+      if (groups.length == calls.length) {
+        calls
+      } else {
+        val grouped = groups.zipWithIndex.map { case (group, i) =>
+          splitCall(s"${funcName}_group${level}_$i",
+            group.flatMap(_.arguments).distinctBy(_._2).toSeq, returnType,
+            makeSplitFunction(foldFunctions(group.map(_.call).toSeq)), audited)
+        }.toSeq
+        groupSplitCalls(grouped, funcName, returnType, makeSplitFunction, 
foldFunctions,
+          callsLeft, level + 1, audited)
+      }
+    }
+  }
+
+  /** The parameter slots of a method taking `arguments`, `this` included. */
+  private def paramSlots(arguments: Seq[(String, String)]): Int =

Review Comment:
   Done. `paramSlots` is gone. `wholeStageSplitArguments` checks the values 
with `calculateParamLengthFromExprValues`, and the group check uses a new 
`calculateParamLengthFromTypes`, which both of the existing calculators share 
for the per-type count. `addNewFunctionTaking` adds a function and registers it 
in `functionsTakingRow` where it takes `INPUT_ROW`. `CommonExprSlots.build` and 
`splitCall` both use it.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -210,9 +339,10 @@ class CodegenContext extends Logging {
 
   /**
    * 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.
+   * whether this row has computed them yet, and the definition, with its id, 
to compute them from.
    */
   case class CommonExprSlots(
+      id: Long,

Review Comment:
   Removed.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1823,6 +2444,96 @@ object CodeGenerator extends Logging {
   // method which is going to be called by the outer class instead of the many 
small ones
   final val MERGE_SPLIT_METHODS_THRESHOLD = 3
 
+  // 
---------------------------------------------------------------------------------------------

Review Comment:
   Moved: the whole group, header included, is now at the end of `object 
CodeGenerator`. The JVM-limit constants are one run again, as on master.
   



-- 
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]

Reply via email to