Search code examples
scalapattern-matchingmatchcase-class

Does the order of alternatives in a Scala match expression matter in terms of performance?


In particular with respect to pattern matching and case classes. Consider the following:

abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

object Expr {
  def simplify(expr: Expr): Expr = expr match {
    // Some basic simplification rules...
    case UnOp("-", UnOp("-", e)) => simplify(e) // Double negation
    case BinOp("+", e, Number(0)) => simplify(e) // Adding zero
    case BinOp("-", e, Number(0)) => simplify(e) // Subtracting zero
    case BinOp("*", e, Number(1)) => simplify(e) // Multiplying by one
    case BinOp("*", e, Number(0)) => Number(0) // Multiplying by zero
    case _ => expr // Default, could not simplify given above rules
  }
}

Given any sample call, say, simplify(UnOp("-", UnOp("-", UnOp("-", UnOp("-", Var("x")))))) (which results in Var("x")), does the order of the alternatives in the match expression matter for performance?

Side note, kind of related (my own observation): One thing that really strikes me about simplify is that it is a recursive function, although unlike other recursive functions I've written / dealt with, the base case comes last in order to avoid terminating early.


Solution

  • Theoretically yes, because matching tests are done in order.

    But in practice the difference can be unsignificant. I've run a micro-benchmark using Caliper and your example. I prefixed Var("x") with 100'000 Unops to make it bigger.

    The results are:

    [info]  0% Scenario{vm=java, trial=0, benchmark=ForFirst} 240395.82 ns; σ=998.55 ns @ 3 trials
    [info] 50% Scenario{vm=java, trial=0, benchmark=ForLast} 251303.52 ns; σ=2342.60 ns @ 5 trials
    [info] 
    [info] benchmark  us linear runtime
    [info]  ForFirst 240 ============================
    [info]   ForLast 251 ==============================
    

    In first test, UnOp case is the first one, in second test its the last one (just before the default case).

    As you can see, it does not really matter (less than 5% slower). Perhaps that, with a huge list of case the order matters, but it would also be a candidate for refactoring.

    Full code is here: https://gist.github.com/1152232 (runs via scala-benchmarking-template).