Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On the performance of copying case classes

I have two case classes: addSmall and addBig. addSmall contains only one field. addBig contains several fields.

case class AddSmall(set: Set[Int] = Set.empty[Int]) {
  def add(e: Int) = copy(set + e)
}

case class AddBig(set: Set[Int] = Set.empty[Int]) extends Foo {
  def add(e: Int) = copy(set + e)
}

trait Foo {
  val a = "a"; val b = "b"; val c = "c"; val d = "d"; val e = "e"
  val f = "f"; val g = "g"; val h = "h"; val i = "i"; val j = "j"
  val k = "k"; val l = "l"; val m = "m"; val n = "n"; val o = "o"
  val p = "p"; val q = "q"; val r = "r"; val s = "s"; val t = "t"
}

A quick benchmark using JMH shows that copying addBig objects is way more exprensive even if i change only one field..

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

@State(Scope.Benchmark)
class AddState {
  var elem: Int = _
  var addSmall: AddSmall = _
  var addBig: AddBig = _

  @Setup(Level.Trial)
  def setup(): Unit = {
    addSmall = AddSmall()
    addBig = AddBig()
    elem = 1
  }
}

@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Array(Mode.Throughput))
class SetBenchmark {
  @Benchmark
  def addSmall(state: AddState): AddSmall = {
    state.addSmall.add(state.elem)
  }

  @Benchmark
  def addBig(state: AddState): AddBig = {
    state.addBig.add(state.elem)
  }
}

And the results show that copying addBig is more than 10 times slower than copying addSmall!

> jmh:run -i 5 -wi 5 -f1 -t1
[info] Benchmark                                   Mode  Cnt       Score       Error   Units
[info] LocalBenchmarks.Set.SetBenchmark.addBig    thrpt    5   10732.569 ±   349.577  ops/ms
[info] LocalBenchmarks.Set.SetBenchmark.addSmall  thrpt    5  126711.722 ± 10538.611  ops/ms

How come copying the object is much slower for addBig? As far as i understand structural sharing, since all fields are immutable copying the object should be very efficient as it only needs to store the changes ("delta") which in this case is only the set s, and should thus give the same performance as addSmall.


EDIT: The same performance issue arises when the state is part of the case class.

case class AddBig(set: Set[Int] = Set.empty[Int], a: String = "a", b: String = "b", ...) {
  def add(e: Int) = copy(set + e)
}
like image 741
Kevin Avatar asked Feb 21 '20 14:02

Kevin


2 Answers

I guess, that this is because AddBig class extends Foo trait, which has all this String fields - a to t. It seems like, in result object they will be declared as regular fields, not the static fields if compare to Java, hence allocating memory for the object, might be the root cause of slower copy performance.

UPDATE: In order to verify this theory you can try to use JOL (Java Object Layout) tool - openjdk.java.net/projects/code-tools/jol

Here is the simple code example:

import org.openjdk.jol.info.{ClassLayout, GraphLayout}
println(ClassLayout.parseClass(classOf[AddSmall]).toPrintable())
println(ClassLayout.parseClass(classOf[AddBig]).toPrintable())

println(GraphLayout.parseInstance(AddSmall()).toPrintable)
println(GraphLayout.parseInstance(AddBig()).toPrintable)

Which in my case produced next output (short version for answer readability):

xample.AddSmall object internals:
 OFFSET  SIZE                             TYPE DESCRIPTION                               VALUE
      0    12                                  (object header)                           N/A
     12     4   scala.collection.immutable.Set AddSmall.set                              N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

example.AddBig object internals:
 OFFSET  SIZE                             TYPE DESCRIPTION                               VALUE
      0    12                                  (object header)                           N/A
     12     4   scala.collection.immutable.Set AddBig.set                                N/A
     16     4                 java.lang.String AddBig.a                                  N/A
     20     4                 java.lang.String AddBig.b                                  N/A
     24     4                 java.lang.String AddBig.c                                  N/A

Instance size: 96 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

example.AddSmall@ea1a8d5d object externals:
          ADDRESS       SIZE TYPE                                     PATH                           VALUE
        770940b28         16 example.AddSmall                                                        (object)
        770940b38     470456 (something else)                         (somewhere else)               (something else)
        7709b38f0         16 scala.collection.immutable.Set$EmptySet$ .set                           (object)


example.AddBig@480bdb19d object externals:
          ADDRESS       SIZE TYPE                                     PATH                           VALUE
        770143658         24 java.lang.String                         .h                             (object)
        770143670         24 [C                                       .h.value                       [h]
        770143688      15536 (something else)                         (somewhere else)               (something else)
        770147338         24 java.lang.String                         .m                             (object)
        770147350         24 [C                                       .m.value                       [m]
        770147368    1104264 (something else)                         (somewhere else)               (something else)
        770254cf0         24 java.lang.String                         .r                             (object)
        770254d08         24 [C                                       .r.value                       [r]
        770254d20    7140768 (something else)                         (somewhere else)               (something else)
        7709242c0         24 java.lang.String                         .a                             (object)

So as you can see fields from parent trait become class fields as well, so will be copied along with the object.

Hope this helps!

like image 192
Ivan Kurchenko Avatar answered Nov 06 '22 04:11

Ivan Kurchenko


Have you checked this question? scala case class copy implementation You can check compiler generated things to elaborate this. There's a probability that these vals became regular fields of case class and being copied each time class copied.

like image 1
Iva Kam Avatar answered Nov 06 '22 03:11

Iva Kam