Does scala cache conversions to functional interfaces

Viewed 95

Scala 2.12 can automatically convert a lambda expression to an interface. E.g, I'm using:

import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer}
import scalapb.GeneratedMessageCompanion

class ProtoSerde[A <: scalapb.GeneratedMessage](implicit companion: GeneratedMessageCompanion[A])
  extends Serde[A] {
  override def serializer(): Serializer[A] = (_: String, data: A) => data.toByteArray

  override def deserializer(): Deserializer[A] = (_: String, data: Array[Byte]) => companion.parseFrom(data)
}

Will it automatically cache the instance of the created objects, so that a new object isn't allocated on every call. I.e. I want:

import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer}
import scalapb.GeneratedMessageCompanion

class ProtoSerde[A <: scalapb.GeneratedMessage](implicit companion: GeneratedMessageCompanion[A])
  extends Serde[A] {
  lazy val _serializer: Serializer[A] = (_: String, data: A) => data.toByteArray
  lazy val _deserializer: Deserializer[A] = (_: String, data: Array[Byte]) => companion.parseFrom(data)

  override def serializer(): Serializer[A] = _serializer

  override def deserializer(): Deserializer[A] = _deserializer
}

Will the compiler perform this optimization automatically, or do I have to do it myself?

3 Answers

def is not memoized to calling it will recompute value every time. It might be later optimized by JIT compiler.

But there's a simpler solution to make properties of class memoized. Scala allows changing def to val while you override parent methods. It is also possible to add lazy modifier on extending class:

class ProtoSerde[A <: scalapb.GeneratedMessage](implicit companion: GeneratedMessageCompanion[A])
  extends Serde[A] {
  override lazy val serializer: Serializer[A] = (_: String, data: A) => data.toByteArray

  override lazy val deserializer: Deserializer[A] = (_: String, data: Array[Byte]) => companion.parseFrom(data)
}

Compiler doesn't perform such optimizations. Conversion lambda expression to an interface doesn't mean saving instance of that interface as a singleton or any another way to save that. It will create this instance every time when you call this lambda. You need to do this optimization by yourself.

Perhaps it is best to benchmark in your particular circumstances. For example, jmh benchmark of the following simplified snippet

trait Quxable[A] {
  def qux(x: A): A
}

class FooImpl1 {
  def zar(): Quxable[Int] = (x: Int) => x + 1
}

class FooImpl2 {
  val _zar: Quxable[Int] = (x: Int) => x + 1
  def zar(): Quxable[Int] = _zar
}

// sbt "jmh:run -i 5 -wi 5 -f 2 -t 1 -prof gc bench.So61983239"

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class So61983239 {
  val end = 1000000
  val fooImpl1 = new FooImpl1
  val fooImpl2 = new FooImpl2

  @Benchmark def impl1 = (1 to end) map (x => fooImpl1.zar().qux(x))
  @Benchmark def impl2 = (1 to end) map (x => fooImpl2.zar().qux(x))
}

gives similar allocation rates (gc.alloc.rate) for both

[info] Benchmark                                           Mode  Cnt         Score         Error   Units
[info] So61983239.impl1                                   thrpt   10        76.866 ±       2.690   ops/s
[info] So61983239.impl1:·gc.alloc.rate                    thrpt   10      4790.899 ±     167.414  MB/sec
[info] So61983239.impl1:·gc.alloc.rate.norm               thrpt   10  68637392.621 ±       0.152    B/op
[info] So61983239.impl1:·gc.churn.PS_Eden_Space           thrpt   10      4793.715 ±     174.955  MB/sec
[info] So61983239.impl1:·gc.churn.PS_Eden_Space.norm      thrpt   10  68686591.527 ± 1866568.380    B/op
[info] So61983239.impl1:·gc.churn.PS_Survivor_Space       thrpt   10         6.216 ±       1.411  MB/sec
[info] So61983239.impl1:·gc.churn.PS_Survivor_Space.norm  thrpt   10     89091.175 ±   20368.810    B/op
[info] So61983239.impl1:·gc.count                         thrpt   10       234.000                counts
[info] So61983239.impl1:·gc.time                          thrpt   10      1055.000                    ms
[info] So61983239.impl2                                   thrpt   10        78.027 ±       0.889   ops/s
[info] So61983239.impl2:·gc.alloc.rate                    thrpt   10      4862.226 ±      54.507  MB/sec
[info] So61983239.impl2:·gc.alloc.rate.norm               thrpt   10  68637392.613 ±       0.162    B/op
[info] So61983239.impl2:·gc.churn.PS_Eden_Space           thrpt   10      4895.604 ±     148.716  MB/sec
[info] So61983239.impl2:·gc.churn.PS_Eden_Space.norm      thrpt   10  69105653.917 ± 1668136.154    B/op
[info] So61983239.impl2:·gc.churn.PS_Survivor_Space       thrpt   10         5.582 ±       2.634  MB/sec
[info] So61983239.impl2:·gc.churn.PS_Survivor_Space.norm  thrpt   10     78851.978 ±   37413.327    B/op
[info] So61983239.impl2:·gc.count                         thrpt   10       230.000                counts
[info] So61983239.impl2:·gc.time                          thrpt   10      1073.000                    ms
Related