Skip to content
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

Merged
merged 32 commits into from
Jul 10, 2017

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jul 3, 2017

We add more extensive optimizations to pattern matching.

In particular: Eliminate redundant tests, and merge variables that have the same right-hand side.

Copy link
Member

@dottybot dottybot left a 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:

  1. Separate subject from body with a blank line
  2. When fixing an issue, start your commit message with Fix #<ISSUE-NBR>:
  3. Limit the subject line to 72 characters
  4. Capitalize the subject line
  5. Do not end the subject line with a period
  6. Use the imperative mood in the subject line ("Added" instead of "Add")
  7. Wrap the body at 80 characters
  8. 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
Copy link
Contributor

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DarkDimius DarkDimius modified the milestone: 0.2 Tech Preview Jul 4, 2017
@@ -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
Copy link
Member

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

@odersky
Copy link
Contributor Author

odersky commented Jul 6, 2017

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.

@odersky
Copy link
Contributor Author

odersky commented Jul 7, 2017

For instance, here's a translation of the match expression

xs match {
  case 0 :: Nil => println("1")
  case _ :: Nil => println("2")
  case 0 :: _   => println("3")
  case x :: y   => println("4")
}

Translation:

  case val x17: scala.collection.immutable.List[Int](Test.xs) = Test.xs
  def case16(case x30: scala.collection.immutable.::[Int]): Unit = 
    {
      case val x25: Int = x30.head
      case val x26: scala.collection.immutable.List[Int] = x30.tl$1
      if Nil.==(x26) then 
        {
          println("2")
        }
       else 
        if 0.==(x25) then 
          {
            println("3")
          }
         else 
          {
            case val x: Int = x25
            case val y: scala.collection.immutable.List[Int] = x26
            println("4")
          }
    }
  if x17.$isInstanceOf$[scala.collection.immutable.::[Int]] then 
    {
      case val x27: scala.collection.immutable.::[Int] = 
        x17.asInstanceOf[scala.collection.immutable.::[Int]]
      if 0.==(x27.head) then 
        if Nil.==(x27.tl$1) then 
          {
            println("1")
          }
         else case16(x27)
       else case16(x27)
    }
   else throw new MatchError(x17)
}

@DarkDimius
Copy link
Contributor

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

      if 0.==(x27.head) then 
        if Nil.==(x27.tl$1) then 
          {
            println("1")
          }
         else case16(x27)
       else case16(x27)

It sees that two else branches are the same and reorders statements so that duplicated else branches are joined:

      if (0.==(x27.head)) && (Nil.==(x27.tl$1)) then 
          {
            println("1")
          }
         else case16(x27) //1

This triggers one more rounds of optimizations after inlining case16 that is now called once.

  1. It additionally gets rid of:
  • x27, as an alias to immutable x17
  • x30.head && x27.head are aliased, the second one is removed and the first one is reused. x30.head is always computed on paths that computed x27.head and both are immutable. This gets rid of x25.

There is also an optimization in this code that both simplifier and your optimization have missed. They both compare first element with 0 twice on the path. Simplifier wasn't able to remove it due to rewriting //1 that simplified outer test but made analyzing inner tests more complex. My intention here was to use idempotent expression elimination to remove this duplicated computation but the implementation is still not good enough.

@odersky
Copy link
Contributor Author

odersky commented Jul 9, 2017

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

@DarkDimius
Copy link
Contributor

@odersky, I agree with your guess. Is this branch ready for review?

@odersky odersky mentioned this pull request Jul 9, 2017
odersky added 18 commits July 9, 2017 19:06
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.
odersky added 13 commits July 9, 2017 19:06
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?
Copy link
Contributor

@DarkDimius DarkDimius left a 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 _ =>
Copy link
Contributor

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 =
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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))
Copy link
Contributor

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))
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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
}

Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor

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.

@DarkDimius
Copy link
Contributor

In private discussion we agreed to change
#2829 (comment)

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 ;-).

@DarkDimius
Copy link
Contributor

Otherwise LGTM

@odersky odersky merged commit 8a93f6e into scala:master Jul 10, 2017
@allanrenucci allanrenucci deleted the add-optimize-patmat branch December 14, 2017 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants