Authors Top

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Overview

In this short tutorial, we’ll learn what the @switch annotation is in Scala, along with its semantics and use cases with the help of relevant examples. We’ll also see the compiler behavior in various situations and the associated performance implications.

2. What Is the @switch Annotation in Scala?

The Scala compiler does certain performance optimizations for pattern matching by treating it like a switch statement in Java. The compiler optimizes by compiling the pattern match into a branch table, thus making it much faster than a simple decision tree that uses multiple if statements.

We can apply the @switch annotation to a match statement in Scala. We can use the @switch annotation to make sure that the compiler applies the optimization to our pattern match expression. The compiler emits a warning message if it’s not able to do any optimization.

3. Compiler Behavior and Performance Implications

A match expression can be compiled into three different ways: a tableswitch, lookupswitch, or decision tree (chain of multiple if statements).

Both tableswitch and lookupswitch use branch tables to store case values and pick the matching value based on a label. The Scala compiler decides which one to use based on how contiguous the values are in the case statements. These operations have time complexities O(1) and O(log n), respectively.

3.1. tableswitch

This is the primary and preferred optimization method of the two. It uses a table of values with labels to pick corresponding values. Then these labels are used for a computed direct jump. Therefore, it’s a constant time O(1) operation, and it’s a faster optimization than lookupswitch. The case statements’ values must be contiguous for this optimization to happen.

Let’s look at an example of pattern matching in which the case values are continuous integers without any gaps:

val numWheels = 2

(numWheels : @switch) match {
  case 1 => println("MonoWheeler")
  case 2 => println("TwoWheeler")
  case 3 => println("ThreeWheeler")
  case 4 => println("FourWheeler")
  case _ => println("UnKnown")
}

Let’s disassemble our class file using javap command and look at the bytecode:

16: tableswitch   { // 1 to 6
               1: 56
               2: 67
               3: 78
               4: 89
               5: 111
               6: 100
         default: 111
    }

We can see that the compiler has applied the optimization as we expected.

3.2. lookupswitch

If the case statements’ values are not contiguous and have gaps in between, the compiler will use a lookupswitch instead of a tableswitch. Here, the compiler uses sorted keys and labels for lookup. Then, it applies a binary search on the sorted keys to finding a matching value. Hence, it’s an O(log n) operation.

Let’s look at an example of pattern matching of non-contiguous char values:

val alphabet = 'A'

(alphabet : @switch) match {
  case 'A' => println("ANIMAL")
  case 'E' => println("ELEPHANT")
  case 'I' => println("IRON")
  case 'O' => println("OWL")
  case 'U' => println("UMBRELLA")
  case _ => println("Not a Vowel")
}

Let’s look at the bytecode once again:

12: lookupswitch  { // 5
              65: 64
              69: 75
              73: 86
              79: 97
              85: 108
         default: 119
    }

This time, we observe that the compiler has used a lookupswitch instead of a tableswitch.

4. Necessary Conditions to Apply the @switch Optimization

We’ve seen how @switch can lead to optimizations in some simple examples. However, there are certain caveats for these optimizations. The Scala compiler applies the @switch optimization only if the following conditions are satisfied:

  • All the case values in the match statement must be Integer types or those types that can safely fit in an integer such as char, short, byte, and bool. Short and Byte are excluded from the list because of the restrictions on type-checks as mentioned in the next point.
  • The match expression cannot have any type-checks, if statements, or extractors.
  • The expression should be literal values instead of reference values. It should not be a computed value and must be available during compilation time.
  • More than two case statements should be present. There’s no performance benefit if there are only two or fewer case statements.

Let’s observe the compiler behavior when we apply the @switch annotation to a match statement using reference values:

val gender = 1
val male = 0
val female = 1

(gender : @switch) match {
  case `male` => println("Male")
  case `female` => println("Female")
  case _ => println("Invalid")
}

We’ll get a warning message from the compiler indicating that it couldn’t emit the @switch optimization:

NotOptimizedExample.scala:10: warning: could not emit switch for @switch annotated match
  (gender : @switch) match {    // Compiler gives a warning: could not emit switch for @switch annotated match
             ^
one warning found

5. Conclusion

In this tutorial, we’ve seen how Scala’s @switch annotation helps us to ensure the compiler is optimizing for our pattern match statements. We learned the performance gains we get by using a tableswitch or a lookupswitch.

Then we listed all the cases in which the compiler won’t apply any optimizations and thus uses simple branching statements.

As usual, the full source code can be found over on GitHub.

Authors Bottom

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!