-
Notifications
You must be signed in to change notification settings - Fork 28.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SPARK-18016][SQL] Code Generation: Constant Pool Limit - reduce entries for mutable state #19811
Conversation
Test build #84154 has finished for PR 19811 at commit
|
Test build #84163 has finished for PR 19811 at commit
|
Test build #84167 has finished for PR 19811 at commit
|
Test build #84169 has finished for PR 19811 at commit
|
Jenkins, retest this please |
Test build #84179 has finished for PR 19811 at commit
|
Test build #84185 has finished for PR 19811 at commit
|
Test build #84186 has finished for PR 19811 at commit
|
Test build #84193 has finished for PR 19811 at commit
|
Test build #84194 has finished for PR 19811 at commit
|
Test build #84214 has finished for PR 19811 at commit
|
// identify multi-dimensional array or no simply-assigned object | ||
!isPrimitiveType(javaType) && | ||
(javaType.contains("[][]") || | ||
!initCode.matches("(^[\\w_]+\\d+\\s*=\\s*null;|" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a bit scared by relying on such regexp. May I ask you why they are needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me borrow explanation from #19518. These regexps try to detect the following cases:
- Object state of like-type initialized to null
- Object state of like-type initialized to the type's base (no-argument) constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the explanation. Still, I can't understand the reason of this. Isn't enough that the init code used is always the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a conservative guard. I think that this intends to avoid unexpected behavior by moving places of initialization from (implicit) constructor to another place. cc @bil
For example, if a statement for initialization refers to a variable, we have to guarantee the variable is not changed from the original place to the new place. It seems to be hard.
WDYT? cc @bdrillard
P.S. I noticed that such a guard is required for primitive value cases, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mgaido91, could you describe what you mean by "Isn't it enough that the init code used is always the same?" There are definitely some complicated init codes used throughout the codebase where, I think as @kiszk was saying, the initcode makes use of a previously defined variable.
Really it would be nice if we had a way of knowing whether an initialization was simple (assigned to a default for primitives, or null or the 0-parameter constructor for objects). Maybe we could define an abstract InitCode
holding a single code
field and then extend that with Simple
and NonSimple
case classes, then we could pattern match on the additional type information rather than trying to regex match the code itself. That solution might be safer and more concrete, but I don't know if it saves us any of the messiness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bdrillard I mean that in https://github.com/apache/spark/pull/19811/files#diff-8bcc5aea39c73d4bf38aef6f6951d42cR248 we are using the init code to initialize together the variables with the same code. If there are init codes which use previously defined variables, their init code would differ from all the other (unless the same previously defined variable is used), thus I don't see the problem.
Instead I do see the problem that in this way we might change the initialization order and this could be a problem. But I think that this problem can be present also in the current implementation, since we are actually changing the order in which things are inited, isn't it?
So, I am thinking, why aren't we initing the arrays as all the other variables so far (ie. as they weren't arrays, as before this PR, one piece of code after the other, without any for loop) and splitting the init code to avoid it to grow beyond the 64 KB limit?
varName | ||
} else { | ||
// Create an initialization code agnostic to the actual variable name which we can key by | ||
val initCodeKey = initCode.replaceAll(varName, "*VALUE*") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about codeFunctions("")
? It looks safer to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am afraid about the side-effect codeFunction("")
. codeFunction
may different result from the result in the first call. Thus, I want to call codeFunction()
only once.
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand your worries. Before the change we were simply passing a string. I don't see how this function can have side-effects, therefore. Have you something specific in mind? Some cases when this might happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for my weak explanation.
There are three cases.
1.
v => {
nameInGlobal = ctx.fresh("tmp")
$v = $name + 1;
}
v => {
val name = ctx.fresh("tmp")
hashInGlobal += name -> v
$v = $name + 1;
}
- other cases
You are right. I have not seen them now, but we cannot guarantee it would not happen.
My question is that what problem you saw by reusing the result of codeFunctions(varName)
? Would it be possible to share it among us?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't understand these cases at the moment, I need to read them carefully, but I can answer to your question for the moment: the problem I see in that replacing a string like that may have undesired effects, for instance, if we have two inits like String varName1 = "varName1";
and String varName2 = "varName2";
they would end up in the same initCodeKey
even though they shouldn't. This can be an extreme and very rare case, but it could happen.
// for type and initialized code. In addition, type, array name, and qualified initialized | ||
// code is stored for code generation | ||
val arrayName = freshName("mutableStateArray") | ||
val qualifiedInitCode = initCode.replaceAll( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here I'd prefer codeFunctions(s"$arrayName[${CodeGenerator.INIT_LOOP_VARIABLE_NAME}]")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am afraid about the side-effect codeFunction(""). codeFunction may different result from the result in the first call. Thus, I want to call codeFunction() only once.
WDYT?
do we have to initialize the elements in a loop? Can we just initialize them one by one like
is it also to reduce constant pool entries? |
javaType: String, | ||
variableName: String, | ||
codeFunctions: String => String = _ => "", | ||
inline: Boolean = false): String = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also a bit scared about using regex to match simple assignment. Maybe we should have 2 versions of addMutableState
: one provides a simple initial value, one provides arbitrary initializing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of possible approaches. Let me count the number of calls of each type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we go this way, what about creating a MutableStateBuilder
then, with methods like withSimpleInitialValue
taking a string and withComplexInit
taking a function or something like that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed there are only about 15 complex assignment cases. I will specify inline = true
for such as case and eliminate conditions using regex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kiszk thanks! A big 👍 for eliminating the regexps! :) I'd prefer something more explicit about the reason why it is inlined, like the solutions proposed above. I think readability would be better. WDYT?
@cloud-fan yes, I have the same opinion and I'd suggest the same thing (#19811 (comment)), at least at the beginning. Then if we find smarter way yo behave we can submit another PR later. |
I think that it is good to have a loop to mainly reduce the byte code size and to reduce constant pool entries.
will not consume constant pool entries for constants (0 - 32767). I like to have a loop as possible. WDYT? |
@@ -168,6 +166,21 @@ class CodegenContext { | |||
val mutableStates: mutable.ArrayBuffer[(String, String, String)] = | |||
mutable.ArrayBuffer.empty[(String, String, String)] | |||
|
|||
// An array keyed by the tuple of mutable states' types and initialization code, holds the | |||
// current max index of the array | |||
var mutableStateArrayIdx: mutable.Map[(String, String), Int] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the below code, looks like this is keyed by (javaType, arrayName)
, instead of (javaType, initCode)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, old comment still exists there. Thanks
|
||
// An array keyed by the tuple of mutable states' types, array names and initialization code, | ||
// holds the code that will initialize the mutableStateArray when initialized in loops | ||
var mutableStateArrayInitCodes: mutable.ArrayBuffer[(String, String, String)] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also keyed by (javaType, arrayName)
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is ok to use an array since this is not looked up. This is used only for generating code.
val qualifiedInitCode = initCode.replaceAll( | ||
varName, s"$arrayName[${CodeGenerator.INIT_LOOP_VARIABLE_NAME}]") | ||
mutableStateArrayCurrentNames += (javaType, initCodeKey) -> arrayName | ||
mutableStateArrayInitCodes += ((javaType, arrayName, qualifiedInitCode)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do all variables in the same array use the same init code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If they are in the same entry of mutableStateArrayInitCodes
, yes. arrayName
is assigned based on initCodeKey
.
val loopIdxVar = CodeGenerator.INIT_LOOP_VARIABLE_NAME | ||
s""" | ||
for (int $loopIdxVar = 0; $loopIdxVar < $arrayName.length; $loopIdxVar++) { | ||
$qualifiedInitCode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have two variables in the same array, they have different init codes, how does this loop work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. I see. You put the variables with same init code in the same array.
I'm afraid this style initialization int[] ints = new int[1001];
ints[0] = 3;
ints[1] = 1;
ints[2] = 5;
... produces too many bytecodes. |
I think we should do optimization incrementally. From
to
already hit our goal to reduce constant pool size, reducing the byte code size of constructor is another story. |
addMutableState(javaType(expr.dataType), value, | ||
s"$value = ${defaultValue(expr.dataType)};") | ||
addMutableState(JAVA_BOOLEAN, isNull, forceInline = true, useFreshName = false) | ||
addMutableState(javaType(expr.dataType), value, forceInline = true, useFreshName = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we do
val isNull = addMutableState(JAVA_BOOLEAN, "subExprIsNull")
val value = addMutableState(javaType(expr.dataType), "subExprValue")
val fn = ...
at the beginning?
Test build #84962 has finished for PR 19811 at commit
|
I think this failure due to R package issue that we have seen before.
|
Another PR also causes the same failure while that PR just added new tests. |
I have to revert that PR again. e58f275 I think it is caused by that PR. Thus, I revert it again. Although I am not very sure what is the root cause. Will try to investigate more. |
ping @cloud-fan |
* compacted. Please set `true` into forceInline, if you want to access the | ||
* status fast (e.g. frequently accessed) or if you want to use the original | ||
* variable name | ||
* @param useFreshName If this is false and forceInline is true, the name is not changed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
more accurate: If this is false and the mutable state ends up inlining in the outer class, the name is not changed
@@ -217,7 +217,7 @@ case class Stack(children: Seq[Expression]) extends Generator { | |||
ctx.addMutableState( | |||
s"$wrapperClass<InternalRow>", | |||
ev.value, | |||
s"${ev.value} = $wrapperClass$$.MODULE$$.make($rowData);") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can localize the global variable ev.value
here to save one global variable slot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will do it in another PR after merging this.
ctx.addMutableState(patternClass, pattern, | ||
s"""$pattern = ${patternClass}.compile("$regexStr");""") | ||
val pattern = ctx.addMutableState(patternClass, "patternLike", | ||
v => s"""$v = ${patternClass}.compile("$regexStr");""", forceInline = true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a clear rule when a global variable should be inlined for better performance? e.g. a microbenchmark showing noteworthy difference definitely proves we should inline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now, we have three rules to apply inlining
- Have to use the original name
- Frequently
usedreferenced, but generated once, in the hot spot - Not expected to be frequently generated proposed by @viirya
Now, we have no rule for 2. I will try to run microbenchmark for 2. Is it better to add these benchmarks into the benchmark directory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, if it's not a lot of them...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my only concern is about point 2. I think it is a dangerous thing to do. What if we generate a lot of frequently used variable? I think it is safer at the moment to consider only 1 and 3 in the decision whether to inline or not. In the future, with a different codegen method, we might then define a threshold over which we generate an array for the given class, otherwise we use plain variables, which IMHO would be the best option but at the moment it is not feasible...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for confusing you about 2. I updated the statement. It is frequently referenced, but generated once. Therefore, if we have advantage for performance, we think it is safer since 3. will also apply inline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If 3 is a precondition for 2, then it is ok. Thanks for the explanation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on comments, I simplified rules.
- Use the original name
- Expect to be not-frequently used.
In the latter, I put comment regarding the reason at each site .
} | ||
assert(ctx1.inlinedMutableStates.size == CodeGenerator.OUTER_CLASS_VARIABLES_THRESHOLD) | ||
// When the number of primitive type mutable states is over the threshold, others are | ||
// allocated into an array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some notes: It's better if we can collect all mutable states before deciding which one should be inlined. However it's impossible to do with the current string based codegen framework, we need to decide inline or not immediately. We can revisit this in the future when we have an AST based codegen framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree. In the future, we hope we have an AST based codegen framework.
val metrics = ctx.addMutableState(classOf[TaskMetrics].getName, "metrics", | ||
v => s"$v = org.apache.spark.TaskContext.get().taskMetrics();", forceInline = true) | ||
val sortedIterator = ctx.addMutableState("scala.collection.Iterator<UnsafeRow>", "sortedIter", | ||
forceInline = true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks reasonable as it's very unlikely we have a lot of sort operators in one stage. We have to inline it manually as we don't have the ability to find this out automatically yet. Same as https://github.com/apache/spark/pull/19811/files#r157408804
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one question: is there any other places like this? do you have a list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Right now, Range is only used when there is one upstream. | ||
ctx.addMutableState("scala.collection.Iterator", input, s"$input = inputs[0];") | ||
val input = ctx.addMutableState("scala.collection.Iterator", "input", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems it's never used
LGTM except some minor comments, great job! |
* is less than `CodeGenerator.OUTER_CLASS_VARIABLES_THRESHOLD` | ||
* 3. its type is multi-dimensional array | ||
* A primitive type variable will be inlined into outer class when the total number of | ||
* When a variable is compacted into an array, the max size of the array for compaction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sentences looks broken? I.e., ...total number of
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this line A primitive type variable will be inlined into outer class when the total number of
looks redundant.
LGTM too, thanks @kiszk and @bdrillard! This is a very important PR IMHO |
LGTM with one minor comment. |
Test build #85107 has finished for PR 19811 at commit
|
|
||
/** | ||
* Returns the reference of next available slot in current compacted array. The size of each | ||
* compacted array is controlled by the config `CodeGenerator.MUTABLESTATEARRAY_SIZE_LIMIT`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: CodeGenerator.MUTABLESTATEARRAY_SIZE_LIMIT
is not a config
|
||
if (right.foldable) { | ||
val rVal = right.eval() | ||
if (rVal != null) { | ||
val regexStr = | ||
StringEscapeUtils.escapeJava(escape(rVal.asInstanceOf[UTF8String].toString())) | ||
ctx.addMutableState(patternClass, pattern, | ||
s"""$pattern = ${patternClass}.compile("$regexStr");""") | ||
// inline mutable state since not many Like operations in a task |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not very sure about this, since Like
is an expression and can appear many times, like other expressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
|
||
if (right.foldable) { | ||
val rVal = right.eval() | ||
if (rVal != null) { | ||
val regexStr = | ||
StringEscapeUtils.escapeJava(rVal.asInstanceOf[UTF8String].toString()) | ||
ctx.addMutableState(patternClass, pattern, | ||
s"""$pattern = ${patternClass}.compile("$regexStr");""") | ||
// inline mutable state since not many RLike operations in a task |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
// Right now, InputAdapter is only used when there is one input RDD. | ||
ctx.addMutableState("scala.collection.Iterator", input, s"$input = inputs[0];") | ||
// inline mutable state since an inputAdaptor in a task |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
ctx.addMutableState(fastHashMapClassName, fastHashMapTerm, | ||
s"$fastHashMapTerm = new $fastHashMapClassName();") | ||
ctx.addMutableState(s"java.util.Iterator<InternalRow>", iterTermForFastHashMap) | ||
fastHashMapTerm = ctx.addMutableState(fastHashMapClassName, "vectorizedHastHashMap", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shall we force inline it too?
sorterTerm = ctx.freshName("sorter") | ||
ctx.addMutableState(classOf[UnsafeKVExternalSorter].getName, sorterTerm) | ||
hashMapTerm = ctx.addMutableState(hashMapClassName, "hashMap", | ||
v => s"$v = $thisPlan.createHashMap();") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
ctx.addMutableState(clsName, matches, | ||
s"$matches = new $clsName($inMemoryThreshold, $spillThreshold);") | ||
val matches = ctx.addMutableState(clsName, "matches", | ||
v => s"$v = new $clsName($inMemoryThreshold, $spillThreshold);") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
great, merging to master, thank you all! We can address other minor comments in follow-ups |
Thank you very much for your support and merging this. I will open a follow-up PR soon. |
…reduce entries for mutable state ## What changes were proposed in this pull request? This PR addresses additional review comments in apache#19811 ## How was this patch tested? Existing test suites Author: Kazuaki Ishizaki <[email protected]> Closes apache#20036 from kiszk/SPARK-18066-followup.
What changes were proposed in this pull request?
This PR is follow-on of #19518. This PR tries to reduce the number of constant pool entries used for accessing mutable state.
There are two directions:
mutableStateArray[32767]
).Here are some discussions to determine these directions.
This PR modifies
addMutableState
function in theCodeGenerator
to check if the declared state can be easily initialized compacted into an array. We identify three types of states that cannot compacted:inline = true
When
useFreshName = false
, the given name is used.Many codes were ported from #19518. Many efforts were put here. I think this PR should credit to @bdrillard
With this PR, the following code is generated:
How was this patch tested?
Added a new test case to
GeneratedProjectionSuite