Search code examples
scalaperformancewrapperscala-collectionsscala-java-interop

Costly performance of CollectionConverters despite being wrappers?


AFAIU, CollectionConverters are wrappers simply delegating calls to the underlying collection, thus the cost should be single object allocation and then single method call indirection, for example, Conversion Between Java and Scala Collections states

Internally, these conversion work by setting up a “wrapper” object that forwards all operations to the underlying collection object. So collections are never copied when converting between Java and Scala.

Analysing SetWrapper indeed we see it simply delegates the call to the underlying collection

class SetWrapper[A](underlying: Set[A]) extends ju.AbstractSet[A] with Serializable { self =>
  ...
  def size = underlying.size
  ...
}

However, consider the following jmh benchmark

import org.openjdk.jmh.annotations._
import scala.jdk.CollectionConverters._
import java.{util => ju}

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class So31830028 {
  val size = 1000000
  val scalaSet: Set[Int] = (1 to size).toSet
  val javaSet: ju.Set[Int]  = (1 to size).toSet.asJava

  @Benchmark def scala = scalaSet.size
  @Benchmark def scalaAsJava = scalaSet.asJava.size
  @Benchmark def java = javaSet.size
  @Benchmark def javaAsScala = javaSet.asScala.size
}

where sbt "jmh:run -i 10 -wi 5 -f 2 -t 1 bench.So31830028" gives

[info] Benchmark                     Mode  Cnt          Score          Error   Units
[info] So31830028.java              thrpt   20  356515729.840 ± 64691657.672   ops/s
[info] So31830028.javaAsScala       thrpt   20  270053471.338 ± 36854051.611   ops/s
[info] So31830028.scala             thrpt   20  448415156.726 ± 53674976.259   ops/s
[info] So31830028.scalaAsJava       thrpt   20  211808793.234 ± 57898858.737   ops/s

Why there seems to be a significant performance penalty if CollectionConverters are simple wrappers?

The question is inspired by Cost of implicit conversion from java to scala collections


Solution

  • In 2.12, scalaSet.size is just a field access; for 2.13 it's a trivial field accessor call on a final class, which should be very easy to inline, so

    a single object allocation and then single method call indirection

    can be a non-trivial overhead compared to that.

    What I don't understand is the difference between java and javaAsScala (edit: duh, it's one more indirection).