1. Introduction

Generics are a really handy and flexible feature in Scala that helps us to avoid code repetition, while still providing type safety.

This flexibility comes at a price. In this tutorial, we’ll look at how to improve generics’ performance using the @specialized feature.

2. Type Erasure

Let’s start by defining a generic class and see what the compiled code looks like:

class NotSpecialized[T] {
  def get(value: T): T = value
}

We can now print the result of the code compilation running scalac -print NotSpecialized.scala:

package <empty> {
  class NotSpecialized extends Object {
    def get(value: Object): Object = value;
    def <init>(): NotSpecialized = {
      NotSpecialized.super.<init>();
      ()
    }
  }
}

In the above example, we can see type-erasure taking place. The type parameter is erased and replaced by its upper-bound primitive type Object. Every time the program is executed, Object will need to be boxed/unboxed to run with the primitive type of invocation.

This is clearly an expensive computation and can lead to performance degradation with large collections.

3. @specialized in Action

Let’s have a look at how we can use the @specialized annotation to improve our code performance.

3.1. Specializing All the Primitive Types

By annotating the T parameter, the compiler will generate a class for each primitive type. Let’s see it in action, adding a specialized class for all the native types:

class AllTypesSpecialized[@specialized T] {
  def get(value: T): T = value
}

Let’s have a look at the compiled code:

package <empty> {
  class AllTypesSpecialized extends Object {
    def get(value: Object): Object = value;
    <specialized> def get$mcZ$sp(value: Boolean): Boolean = scala.Boolean.unbox(AllTypesSpecialized.this.get(scala.Boolean.box(value)));
    <specialized> def get$mcB$sp(value: Byte): Byte = scala.Byte.unbox(AllTypesSpecialized.this.get(scala.Byte.box(value)));
    <specialized> def get$mcC$sp(value: Char): Char = scala.Char.unbox(AllTypesSpecialized.this.get(scala.Char.box(value)));
    <specialized> def get$mcD$sp(value: Double): Double = scala.Double.unbox(AllTypesSpecialized.this.get(scala.Double.box(value)));
    <specialized> def get$mcF$sp(value: Float): Float = scala.Float.unbox(AllTypesSpecialized.this.get(scala.Float.box(value)));
    <specialized> def get$mcI$sp(value: Int): Int = scala.Int.unbox(AllTypesSpecialized.this.get(scala.Int.box(value)));
    <specialized> def get$mcJ$sp(value: Long): Long = scala.Long.unbox(AllTypesSpecialized.this.get(scala.Long.box(value)));
    <specialized> def get$mcS$sp(value: Short): Short = scala.Short.unbox(AllTypesSpecialized.this.get(scala.Short.box(value)));
    <specialized> def get$mcV$sp(value: scala.runtime.BoxedUnit): Unit = {
      AllTypesSpecialized.this.get(value);
      ()
    };
    def <init>(): AllTypesSpecialized = {
      AllTypesSpecialized.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcB$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Byte): Byte = AllTypesSpecialized$mcB$sp.this.get$mcB$sp(value);
    override <specialized> def get$mcB$sp(value: Byte): Byte = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Byte.box(AllTypesSpecialized$mcB$sp.this.get(scala.Byte.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcB$sp = {
      AllTypesSpecialized$mcB$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcC$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Char): Char = AllTypesSpecialized$mcC$sp.this.get$mcC$sp(value);
    override <specialized> def get$mcC$sp(value: Char): Char = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Char.box(AllTypesSpecialized$mcC$sp.this.get(scala.Char.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcC$sp = {
      AllTypesSpecialized$mcC$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcD$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Double): Double = AllTypesSpecialized$mcD$sp.this.get$mcD$sp(value);
    override <specialized> def get$mcD$sp(value: Double): Double = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Double.box(AllTypesSpecialized$mcD$sp.this.get(scala.Double.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcD$sp = {
      AllTypesSpecialized$mcD$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcF$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Float): Float = AllTypesSpecialized$mcF$sp.this.get$mcF$sp(value);
    override <specialized> def get$mcF$sp(value: Float): Float = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Float.box(AllTypesSpecialized$mcF$sp.this.get(scala.Float.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcF$sp = {
      AllTypesSpecialized$mcF$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcI$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Int): Int = AllTypesSpecialized$mcI$sp.this.get$mcI$sp(value);
    override <specialized> def get$mcI$sp(value: Int): Int = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Int.box(AllTypesSpecialized$mcI$sp.this.get(scala.Int.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcI$sp = {
      AllTypesSpecialized$mcI$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcJ$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Long): Long = AllTypesSpecialized$mcJ$sp.this.get$mcJ$sp(value);
    override <specialized> def get$mcJ$sp(value: Long): Long = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Long.box(AllTypesSpecialized$mcJ$sp.this.get(scala.Long.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcJ$sp = {
      AllTypesSpecialized$mcJ$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcS$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Short): Short = AllTypesSpecialized$mcS$sp.this.get$mcS$sp(value);
    override <specialized> def get$mcS$sp(value: Short): Short = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Short.box(AllTypesSpecialized$mcS$sp.this.get(scala.Short.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcS$sp = {
      AllTypesSpecialized$mcS$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcV$sp extends AllTypesSpecialized {
    override <specialized> def get(value: scala.runtime.BoxedUnit): Unit = AllTypesSpecialized$mcV$sp.this.get$mcV$sp(value);
    override <specialized> def get$mcV$sp(value: scala.runtime.BoxedUnit): Unit = ();
    override <bridge> <specialized> <artifact> def get(value: Object): Object = {
      AllTypesSpecialized$mcV$sp.this.get(value.$asInstanceOf[scala.runtime.BoxedUnit]());
      scala.runtime.BoxedUnit.UNIT
    };
    <specialized> def <init>(): AllTypesSpecialized$mcV$sp = {
      AllTypesSpecialized$mcV$sp.super.<init>();
      ()
    }
  };
  <specialized> class AllTypesSpecialized$mcZ$sp extends AllTypesSpecialized {
    override <specialized> def get(value: Boolean): Boolean = AllTypesSpecialized$mcZ$sp.this.get$mcZ$sp(value);
    override <specialized> def get$mcZ$sp(value: Boolean): Boolean = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Boolean.box(AllTypesSpecialized$mcZ$sp.this.get(scala.Boolean.unbox(value)));
    <specialized> def <init>(): AllTypesSpecialized$mcZ$sp = {
      AllTypesSpecialized$mcZ$sp.super.<init>();
      ()
    }
  }
}

By looking at the compiled code, we can clearly see that the compiler generated a class for each primitive type. This will clearly save time for boxing/unboxing at runtime.

3.2. Specializing Only Some Primitive Types

We can further optimize the code if we know our method will only use a subset of types. Let’s specialize our class for Int and Boolean only:

class OnlyTwoTypesSpecialized[@specialized(Int, Boolean) T] {
  def get(value: T): T = value
}

Finally, let’s print out the compiled code:

package com.baeldung.scala.specialized {
  class OnlyTwoTypesSpecialized extends Object {
    def get(value: Object): Object = value;
    <specialized> def get$mcZ$sp(value: Boolean): Boolean = scala.Boolean.unbox(OnlyTwoTypesSpecialized.this.get(scala.Boolean.box(value)));
    <specialized> def get$mcI$sp(value: Int): Int = scala.Int.unbox(OnlyTwoTypesSpecialized.this.get(scala.Int.box(value)));
    def <init>(): com.baeldung.scala.specialized.OnlyTwoTypesSpecialized = {
      OnlyTwoTypesSpecialized.super.<init>();
      ()
    }
  };
  <specialized> class OnlyTwoTypesSpecialized$mcI$sp extends com.baeldung.scala.specialized.OnlyTwoTypesSpecialized {
    override <specialized> def get(value: Int): Int = OnlyTwoTypesSpecialized$mcI$sp.this.get$mcI$sp(value);
    override <specialized> def get$mcI$sp(value: Int): Int = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Int.box(OnlyTwoTypesSpecialized$mcI$sp.this.get(scala.Int.unbox(value)));
    <specialized> def <init>(): com.baeldung.scala.specialized.OnlyTwoTypesSpecialized$mcI$sp = {
      OnlyTwoTypesSpecialized$mcI$sp.super.<init>();
      ()
    }
  };
  <specialized> class OnlyTwoTypesSpecialized$mcZ$sp extends com.baeldung.scala.specialized.OnlyTwoTypesSpecialized {
    override <specialized> def get(value: Boolean): Boolean = OnlyTwoTypesSpecialized$mcZ$sp.this.get$mcZ$sp(value);
    override <specialized> def get$mcZ$sp(value: Boolean): Boolean = value;
    override <bridge> <specialized> <artifact> def get(value: Object): Object = scala.Boolean.box(OnlyTwoTypesSpecialized$mcZ$sp.this.get(scala.Boolean.unbox(value)));
    <specialized> def <init>(): com.baeldung.scala.specialized.OnlyTwoTypesSpecialized$mcZ$sp = {
      OnlyTwoTypesSpecialized$mcZ$sp.super.<init>();
      ()
    }
  }
}

We can see that the compiler only creates a version for Boolean and one for Int.

We can save time at run-time, but also save space since we will only have the classes for the types we’re interested in.

4. Benchmarking

How much do we really gain when “specializing” our classes? We can easily benchmark it using sbt-jmh.

Let’s write the benchmark code:

package com.baeldung

import java.util.concurrent.TimeUnit
import org.openjdk.jmh.annotations._

@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Array(Mode.All))
class Benchmark_Specialized {

  @Benchmark
  def testNotSpecialized: Unit = {
    val notSpecialized = new NotSpecialized[Int]
    (0 to 1000000).map(notSpecialized.get(_))
  }

  @Benchmark
  def testOnlyTwoTypesSpecialized: Unit = {
    val onlyTwoTypesSpecialized = new OnlyTwoTypesSpecialized[Int]
    (0 to 1000000).map(onlyTwoTypesSpecialized.get(_))
  }

  @Benchmark
  def testAllTypesSpecialized: Unit = {
    val allSpecialized = new AllTypesSpecialized[Int]
    (0 to 1000000).map(allSpecialized.get(_))
  }

}

class AllTypesSpecialized[@specialized T] {
  def get(value: T): T = value
}

class OnlyTwoTypesSpecialized[@specialized(Int, Boolean) T] {
  def get(value: T): T = value
}

class NotSpecialized[T] {
  def get(value: T): T = value
}

We can run the benchmark with:

jmh:run -i 1 -wi 1 -f 1 -rf text .*Specialized.*

Let’s check out the results:

[info] Benchmark                                          Mode  Cnt   Units
[info] Benchmark_Specialized.testNotSpecialized           thrpt 0.169 ops/ms
[info] Benchmark_Specialized.testAllTypesSpecialized      thrpt 0.193 ops/ms
[info] Benchmark_Specialized.testOnlyTwoTypesSpecialized  thrpt 0.198 ops/ms

If we look at the throughput of the different tests, we can clearly see the most specialized version of the code is faster.

By comparing the NotSpecialized and the AllTypesSpecialized version of the code, we’re gaining about 15% performance. By further limiting specialization to only two types, we get a small improvement of about 3%. The most the @specialized annotation gives us is a 17% performance improvement (NotSpecialized vs OnlyTwoTypesSpecialized), not bad at all!

This is worth keeping in mind when performance is important in our application.

5. Conclusion

In this article, we looked at the @specialized annotation and how it can improve performance when using generics. The @specialized annotation can help us in having the best of both worlds in our code.

As always, the code can be found over on GitHub.


» 下一篇: Scala 面试问题