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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1248,7 +1248,67 @@ class CodegenContext extends Logging {
         makeSplitFunction,
         foldFunctions)
 
-      foldFunctions(outerClassFunctionCalls ++ innerClassFunctionCalls)
+      foldFunctions(groupFunctionCalls(
+        (outerClassFunctionCalls ++ innerClassFunctionCalls).toSeq,
+        func,
+        arguments,
+        returnType,
+        makeSplitFunction,
+        foldFunctions))
+    }
+  }
+
+  /**
+   * The calls to the functions `splitExpressions` split out, grouped into 
functions of their own

Review Comment:
   Right, my edit left it detached. Moved it above `groupFunctionCalls` in 
71d702dccdd.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1248,7 +1248,67 @@ class CodegenContext extends Logging {
         makeSplitFunction,
         foldFunctions)
 
-      foldFunctions(outerClassFunctionCalls ++ innerClassFunctionCalls)
+      foldFunctions(groupFunctionCalls(
+        (outerClassFunctionCalls ++ innerClassFunctionCalls).toSeq,
+        func,
+        arguments,
+        returnType,
+        makeSplitFunction,
+        foldFunctions))
+    }
+  }
+
+  /**
+   * 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 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.
+   */
+  /**
+   * How many calls to split functions taking `arguments` fit in one method 
with room to spare
+   * below the 8000 bytes HotSpot compiles; see `groupFunctionCalls`.
+   */
+  private def splitCallsPerMethod(arguments: Seq[(String, String)]): Int = 
math.max(2,

Review Comment:
   Agreed, the budget is per call. `ConcatWs` calls it three times in one 
method on its vararg path, so three call sets each just under the bound could 
together approach the limit. A call measured about 15 bytes in `javap` (see the 
next thread), which puts one ungrouped set at about 2500 bytes at most, so 
three of them leave room for the caller's own code, but not much. I've 
documented this at `splitCallsPerMethod`. Sharing one budget across the calls a 
method makes would need state in the context, and I'd rather keep that for a 
separate change if it ever shows up in practice.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1740,6 +1815,19 @@ 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
 
+  /**
+   * The most bytecode a call to a split function and its fold take in the 
calling method,
+   * besides a load per argument; see `CodegenContext.groupFunctionCalls`.
+   */
+  final val MAX_BYTES_PER_SPLIT_CALL = 20

Review Comment:
   It wasn't when you asked, so I checked it now. I compiled the generated 
classes of the 1000-branch `CASE WHEN` projection and of the 30000-wide 
`Murmur3Hash` and disassembled them with `javap -c -p`. A `CASE WHEN` call with 
its fold (invoke, loads, store, compare, branch) is about 15 bytes, measured 
over the 8-call group methods: 119 bytes for 8 calls. Calls into another nested 
class, with the `aload_0` and two `getfield`s for the instance, are 13 to 15 
bytes: the hash's group methods are 102 to 105 bytes for 7 to 8 calls. So 20 is 
an upper bound with some margin, and the half-limit budget stays on top of 
that. I've added these measurements to the constant's comment.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1314,13 +1374,28 @@ class CodegenContext extends Logging {
 
     val argDefinitionString = arguments.map { case (t, name) => s"$t $name" 
}.mkString(", ")
     val argInvocationString = arguments.map(_._2).mkString(", ")
+    val callsPerMethod = splitCallsPerMethod(arguments)
 
     innerClassToFunctions.flatMap {
       case ((innerClassName, innerClassInstance), innerClassFunctions) =>
         // for performance reasons, the functions are prepended, instead of 
appended,
         // thus here they are in reversed order
         val orderedFunctions = innerClassFunctions.reverse
-        if (orderedFunctions.size > MERGE_SPLIT_METHODS_THRESHOLD) {
+        if (orderedFunctions.size > callsPerMethod) {
+          // Too many calls for one method to stay JIT-compilable: one merged 
method per part,
+          // whose calls the caller groups further if there are still too many 
of them.
+          orderedFunctions.grouped(callsPerMethod).zipWithIndex.map { case 
(part, i) =>

Review Comment:
   Good catch, that's inconsistent with the reasoning for 
`SPLIT_CALLS_PER_GROUP`. The part size only needed to bound bytes, but a part 
of ~170 calls has the same C2 inlining problem as a large group. The parts are 
now of `SPLIT_CALLS_PER_GROUP` calls, and `groupFunctionCalls` handles the 
levels above them. The cost on the side SPARK-22226 cared about is small: each 
part adds one method to the outer class's constant pool, a few entries, instead 
of one per inner class, so about 125 methods for an inner class of 1000 
functions, against the 65535 limit. The `COALESCE` test of 10000 children goes 
through this path.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1248,7 +1248,67 @@ class CodegenContext extends Logging {
         makeSplitFunction,
         foldFunctions)
 
-      foldFunctions(outerClassFunctionCalls ++ innerClassFunctionCalls)
+      foldFunctions(groupFunctionCalls(
+        (outerClassFunctionCalls ++ innerClassFunctionCalls).toSeq,
+        func,
+        arguments,
+        returnType,
+        makeSplitFunction,
+        foldFunctions))
+    }
+  }
+
+  /**
+   * 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 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.
+   */
+  /**
+   * How many calls to split functions taking `arguments` fit in one method 
with room to spare
+   * below the 8000 bytes HotSpot compiles; see `groupFunctionCalls`.
+   */
+  private def splitCallsPerMethod(arguments: Seq[(String, String)]): Int = 
math.max(2,
+    DEFAULT_JVM_HUGE_METHOD_LIMIT / 2 / (MAX_BYTES_PER_SPLIT_CALL + 2 * 
arguments.length))
+
+  private def groupFunctionCalls(
+      calls: Seq[String],
+      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) {
+      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)"

Review Comment:
   Yes, the 30000-wide hash test goes through exactly this path. I disassembled 
its generated classes. The outer class spills into `NestedClass_0` to 
`NestedClass_6`, and all five group methods land in `NestedClass_6` and call 
private split functions of `NestedClass_0` to `_5` and of the outer class, for 
example `computeHash_0_group0_100$` calls into `NestedClass_1` and 
`NestedClass_2`. Janino compiles private methods of nested classes as static 
`name$(outer, ...)` methods, which is what makes the sibling access work, and 
the test checks the hash value against the interpreted one.
   



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CodeGenerationSuite.scala:
##########
@@ -102,6 +102,91 @@ class CodeGenerationSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     assert(actual.head == cases)
   }
 
+  /**
+   * Runs `body` and fails if the code it generates has a method past the 8000 
bytes HotSpot
+   * compiles, which `CodeGenerator` logs as it compiles the class.
+   */
+  private def assertJitCompilable(body: => Unit): Unit = {
+    val appender = new LogAppender("methods too long to be JIT compiled")
+    withLogAppender(appender,
+        loggerNames = Seq(classOf[CodeGenerator[_, _]].getName), level = 
Some(Level.INFO)) {
+      body
+    }
+    val tooLong = appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+      .filter(_.contains("too long to be JIT compiled"))
+    assert(tooLong.isEmpty, tooLong.mkString("\n"))
+  }
+
+  // The tests below pin SPARK-59783: an expression split into hundreds of 
functions left one call
+  // per function in the method holding them, and the calls alone took that 
method past the
+  // 8000 bytes HotSpot compiles, so every row ran it interpreted. Each test 
covers one caller of
+  // `splitExpressions`, with as many children as it takes for that method to 
cross the limit
+  // without the grouping: fewer where one child generates more code.
+
+  test("SPARK-59783: the calls to the functions CASE WHEN splits into are 
JIT-compilable") {
+    val wide = 3000
+    val input = BoundReference(0, IntegerType, nullable = false)
+    val expression = CaseWhen(
+      (1 to wide).map(k => (EqualTo(input, Literal(k)), Multiply(input, 
Literal(k)))), Literal(0))
+    assertJitCompilable {
+      val projection = GenerateMutableProjection.generate(Seq(expression))
+      Seq(7 -> 49, wide -> wide * wide, wide + 1 -> 0).foreach { case (in, 
out) =>
+        assert(projection(new GenericInternalRow(Array[Any](in))).getInt(0) == 
out)
+      }
+    }
+  }
+
+  test("SPARK-59783: the calls to the functions COALESCE splits into are 
JIT-compilable") {
+    val wide = 10000
+    val expression = Coalesce((0 until wide).map(i => BoundReference(i, 
IntegerType, true)))
+    assertJitCompilable {
+      val projection = GenerateMutableProjection.generate(Seq(expression))
+      val values = new Array[Any](wide)
+      assert(projection(new GenericInternalRow(values)).isNullAt(0))
+      values(wide - 1) = 42
+      assert(projection(new GenericInternalRow(values)).getInt(0) == 42)
+    }
+  }
+
+  test("SPARK-59783: the calls to the functions IN splits into are 
JIT-compilable") {
+    val wide = 10000
+    val input = BoundReference(0, IntegerType, nullable = false)
+    val expression = In(input, (1 to wide).map(k => Literal(k * 2)))
+    assertJitCompilable {
+      val projection = GenerateMutableProjection.generate(Seq(expression))
+      Seq(2 -> true, wide * 2 -> true, 3 -> false).foreach { case (in, out) =>
+        assert(projection(new 
GenericInternalRow(Array[Any](in))).getBoolean(0) == out)
+      }
+    }
+  }
+
+  test("SPARK-59783: the calls to the functions a hash splits into are 
JIT-compilable") {

Review Comment:
   Locally the five tests take about 4 s together: `CASE WHEN` 0.4 s, 
`COALESCE` 0.5 s, `IN` 0.3 s, the ordering 0.3 to 0.4 s, and the hash test 2.2 
to 2.4 s for both `Murmur3Hash` and `XxHash64`. Each width is the smallest 
round number at which the test fails without the change. At 10000 children the 
hash test still passes without it, so it runs at 30000. I can bisect between 
the two if you'd like it faster.
   



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