-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Opt: Get rid of the LiftTry phase; instead handle things in the back-end. #18619
Conversation
@WojciechMazur Could you confirm that Scala Native indeed does not care about this? |
@@ -372,10 +375,12 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { | |||
// AbstractValidatingLambdaMetafactory.validateMetafactoryArgs | |||
|
|||
val DesugaredSelect(prefix, _) = fun: @unchecked | |||
genLoad(prefix) | |||
val prefixTK = genLoad(prefix) | |||
stack.push(prefixTK) |
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 was missing before, although it's a bit theoretical, since the arguments to Closure
s are always identifiers in practice, which means they don't contain any Return
nor Try
that would need an accurate stack
.
…end. When we enter a `try-catch` at the JVM level, we have to make sure that the stack is empty. That's because, upon exception, the JVM wipes the stack, and we must not lose operands that are already on the stack that we will still use. Previously, this was achieved with a transformation phase, `LiftTry`, which lifted problematic `try-catch`es in local `def`s, called `liftedTree$x`. It analyzed the tree to predict which `try-catch`es would execute on a non-empty stack when eventually compiled to the JVM. This approach has several shortcomings. It exhibits performance cliffs, as the generated def can then cause more variables to be boxed in to `XRef`s. These were the only extra defs created for implementation reasons rather than for language reasons. As a user of the language, it is hard to predict when such a lifted def will be needed. The additional `liftedTree` methods also show up on stack traces and obfuscate them. Debugging can be severely hampered as well. Phases executing after `LiftTry`, notably `CapturedVars`, also had to take care not to create more problematic situations as a result of their transformations, which is hard to predict and to remember. Finally, Scala.js and Scala Native do not have the same restriction, so they received suboptimal code for no reason. In this commit, we entirely remove the `LiftTry` phase. Instead, we enhance the JVM back-end to deal with the situation. When starting a `try-catch` on a non-empty stack, we stash the entire contents of the stack into local variables. After the `try-catch`, we pop all those local variables back onto the stack. We also null out the leftover vars not to prevent garbage collection. This new approach solves all of the problems mentioned above.
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 seems good to me. I guess the overhead of saving and restoring the stack is OK because try blocks on a non-empty stack are not too common?
I was trying to think of ways this could break, i.e., is there a way that byecode within the try
block could attempt to read something from the stack (which is now no longer there). I don't think so.
It is pretty rare, yes. But anyway, storing the stack in locals cannot be worse than calling an external method. Calling an external method would effectively have to stash the stack into local variables (or callee-saved registers, which is the same thing) at the JVM level.
I don't see how that could happen. |
FTR, in the compiler codebase, there are two try-catch'es that need stashing. One uses 1 local variable: |
Yes, I can confirm we don't relay on LiftTry in ScalaNative and it can be removed |
When we enter a
try-catch
at the JVM level, we have to make sure that the stack is empty. That's because, upon exception, the JVM wipes the stack, and we must not lose operands that are already on the stack that we will still use.Previously, this was achieved with a transformation phase,
LiftTry
, which lifted problematictry-catch
es in localdef
s, calledliftedTree$x
. It analyzed the tree to predict whichtry-catch
es would execute on a non-empty stack when eventually compiled to the JVM.This approach has several shortcomings.
It exhibits performance cliffs, as the generated def can then cause more variables to be boxed in to
XRef
s. These were the only extra defs created for implementation reasons rather than for language reasons. As a user of the language, it is hard to predict when such a lifted def will be needed.The additional
liftedTree
methods also show up on stack traces and obfuscate them. Debugging can be severely hampered as well.Phases executing after
LiftTry
, notablyCapturedVars
, also had to take care not to create more problematic situations as a result of their transformations, which is hard to predict and to remember.Finally, Scala.js and Scala Native do not have the same restriction, so they received suboptimal code for no reason.
In this commit, we entirely remove the
LiftTry
phase. Instead, we enhance the JVM back-end to deal with the situation. When starting atry-catch
on a non-empty stack, we stash the entire contents of the stack into local variables. After thetry-catch
, we pop all those local variables back onto the stack. We also null out the leftover vars not to prevent garbage collection.This new approach solves all of the problems mentioned above.
This could be back-ported to Scala 2 if there is interest.
/cc @adpi2 who wanted this to improve debugging.