-
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
Add pattern matcher optimizations #2829
Conversation
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.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Commit Messages
We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).
Please stick to these guidelines for commit messages:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Added" instead of "Add")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
@@ -70,7 +70,6 @@ class Compiler { | |||
new CrossCastAnd, // Normalize selections involving intersection types. | |||
new Splitter), // Expand selections involving union types into conditionals | |||
List(new VCInlineMethods, // Inlines calls to value class methods | |||
new IsInstanceOfEvaluator, // Issues warnings when unreachable statements are present in match/if 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.
Why is IsInstanceOfEvaluator
removed? I see that there are some modifications to it.
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.
@nicolasstucki See #2820
@@ -530,7 +529,7 @@ object PatternMatcher { | |||
*/ | |||
private def inlineLabelled(plan: Plan) = { | |||
val refCount = referenceCount(plan) | |||
def toDrop(sym: Symbol) = labelled.contains(sym) && refCount(sym) <= 1 | |||
def toDrop(sym: Symbol) = labelled.contains(sym) && refCount(symb) <= 1 |
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 commit introduces a typo
dc8b198
to
f1a669b
Compare
This is ready for review now. With the latest optimizations that do variable propagation across label boundaries the generated code is pretty good now - in fact it looks often better than what one would generate by hand. |
For instance, here's a translation of the match expression
Translation:
|
@odersky, looks really good. Here are several more steps that Simplifier in https://github.com/dotty-linker/dotty/tree/opto is able to additionally do:
It sees that two else branches are the same and reorders statements so that duplicated else branches are joined:
This triggers one more rounds of optimizations after inlining case16 that is now called once.
There is also an optimization in this code that both simplifier and your optimization have missed. They both compare first element with |
@DarkDimius God to see that local opts improves further on this. My guess is it's because local opts do many rounds, whereas pm does just one pass. |
@odersky, I agree with your guess. Is this branch ready for review? |
IsInstanceOfEvaluator as prone to mispredict whether the test was generated by a pattern match or not. We fix this by having a special type test symbol that represents tests generated by a pattern match.
We had: scala> null.isInstanceOf[String] -- Warning: <console>:5:17 ----------------------------------------------------- 5 |null.isInstanceOf[String] |^^^^^^^^^^^^^^^^^^^^^^^^^ |this will always yield true if the scrutinee is non-null, since `Null` is a subclass of `String` (will be optimized away) val res0: Boolean = true scala> val x: AnyRef = null val x: AnyRef = null scala> x.isInstanceOf[String] val res1: Boolean = false Fixed by using `isNullable`.
No need to use inheritance here.
Surprisingly, this caused the IDE to crash with a MatchError. (previous pattern matcher, not new one). I could not work out why. But in any case it was the wrong pattern before.
This now special cases `x.isInstance[y.type]` to `x eq y` and similarly for constant types.
Warn if a match is @switch annotated but we could not transform code to a switch.
The new pattern matcher generates switches only for 3 or more cases. Update bytecode tests accordingly.
- better structure - better names - more extensive documentation
For now we keep the previous one around as PatternMatcherOld.scala
It turns out that when we restrict DropGoodCasts to isInstanceOf tests (as opposed to pattern-matcher generated type tests), we do not miss any error messages. Some warnings are still be omitted under optimize, not sure whether we want to fix that.
It seems there are more issues with local optimizations. I did not track down details but in one case positions were wrong after a rewrite. That was tryPatternMatchError.scala. We should track these problems down in a different PR.
Split constructs for var and label binding into two separate abstractions. Also, more systematic organization of optimizations.
Most optimizations are similar in the way they traverse a plan. This commit factors out the commonalities in a base class `PlanTransform`.
Employing label parameters, we can eliminate redundant expressions systematically. This produces now optimal code for reducable.scala.
The new algorithm systematically avoids references to pattern-generated variables. This leads to nicer types and fewer casts in practice. It also avoids the problem that a type refers to a variable that has been eliminated.
We now demand that the number of different constants in the emitted match is the same as the number of different constants in the original match.
The inline labels optimization is no longer needed. It was also incorrect. To verify: Compile classpath/AggregateClassPath with optimization to witness a crash in the backend: Key not found: case3 The optimization incorrectly eliminates the label even though it is called twice by the generated code. maybe it does not detect case labels called with parameters?
cbf806a
to
e138568
Compare
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.
Minor suggestions, except the one about Case flag.
f1 === f2 && as1.corresponds(as2)(_ === _) | ||
case (TypeApply(f1, ts1), TypeApply(f2, ts2)) => | ||
f1 === f2 && ts1.tpes.corresponds(ts2.tpes)(_ =:= _) | ||
case _ => |
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.
A lot of code in Dotty has a habbit of needlessly wrapping code in Block(Nil, expr).
It would be nice to add a case here that handles blocks to make it more robust.
case _ => | ||
false | ||
} | ||
def hash(implicit ctx: Context): 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.
Dead code?
object PatternMatcher { | ||
import ast.tpd._ | ||
|
||
final val selfCheck = true // debug option, if on we check that no case gets generated twice |
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.
Should we leave it enabled?
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.
No, I think we can drop it. On would typically enable it if something funky happens in the backend with double definitions.
/** Was symbol generated by pattern matcher? */ | ||
private def isPatmatGenerated(sym: Symbol) = | ||
sym.is(Synthetic) && | ||
(sym.is(Label) || sym.name.is(PatMatStdBinderName)) |
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 used to use flag Case
for this case. I propose we continue using it.
Using Label is ambiguous, as other phases can also generate it, e.g. tailrec.
onFailure) | ||
case UnApply(extractor, implicits, args) => | ||
val mt @ MethodType(_) = extractor.tpe.widen | ||
var unapp = extractor.appliedTo(ref(scrutinee).ensureConforms(mt.paramInfos.head)) |
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.
should there be a TypeTest that ensures that scrutinee
conforms to mt.paramInfos.head
?
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 TypeTest is already generated by Typer.
((casegen: Casegen) => combineExtractors(altTreeMakers :+ TrivialTreeMaker(casegen.one(Literal(Constant(true)))))(casegen)) | ||
) | ||
|
||
val find |
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 should also check if initializer refers to vars:
case class Box[T](var a: T)
x match {
case x @ Box(t) => // this can be filtered by checking that t is not synthetic
x.a = null;
t
case Box(Box(t)) => // this can't, as the breakage happens through an alias to the outer
x.asInstanceOf[Box].a = null;
t
}
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.
Github bug? This comment used to refer to different line, that implements inlining of vals.
((casegen: Casegen) => combineExtractors(altTreeMakers :+ TrivialTreeMaker(casegen.one(Literal(Constant(true)))))(casegen)) | ||
) | ||
|
||
val find |
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.
why not do it even if it wasn't annotated similar to how tailrec does it?
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.
Github bug? This comment used to refer to lines deciding to use switch that currently only trigger if user asked for it.
In private discussion we agreed to change to use semantic names instead. I'd also propose to extract this method and make it public so that others can call it, instead of hardcoding the current test. I'll need to modify quite a lot of code to use this test instead of checking for case flag ;-). |
Otherwise LGTM |
We add more extensive optimizations to pattern matching.
In particular: Eliminate redundant tests, and merge variables that have the same right-hand side.