关于复制案例的表现类

On the performance of copying case classes

我有两个案例 classes:addSmalladdBigaddSmall 仅包含一个字段。 addBig 包含多个字段。

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"
}

使用 JMH 的快速基准测试表明,即使我只更改一个字段,复制 addBig 对象的成本也更高..

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)
  }
}

并且结果显示复制addBig比复制addSmall慢10倍以上!

> 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

为什么 addBig 复制对象要慢得多? 据我了解结构共享,因为所有字段都是不可变的,复制对象应该非常有效,因为它只需要存储更改 ("delta"),在这种情况下只是集合 s,并且因此应该提供与 addSmall.

相同的性能

编辑:当状态是案例的一部分时,会出现同样的性能问题class。

case class AddBig(set: Set[Int] = Set.empty[Int], a: String = "a", b: String = "b", ...) {
  def add(e: Int) = copy(set + e)
}

我猜,这是因为 AddBig class 扩展了 Foo 特征,它具有所有这些 String 字段 - at.看起来,在结果对象中,它们将被声明为常规字段,而不是 static 字段(如果与 Java 相比),因此为对象分配内存可能是复制性能较慢的根本原因。

更新: 为了验证这个理论你可以尝试使用JOL(Java Object Layout)工具 - openjdk.java.net/projects/code-tools/jol

这是简单的代码示例:

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)

在我的案例中,它产生了下一个输出(答案可读性的简短版本):

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)

因此,您可以看到来自父特征的字段也变成了 class 字段,因此将与对象一起复制。

希望对您有所帮助!

你检查过这个问题了吗? 您可以检查编译器生成的东西来详细说明这一点。这些 val 有可能成为 case class 的常规字段,并且每次 class 被复制。

您的 Foo 特征为每个子 class 添加 20 个成员,即使它们是常量。这将使用更多内存并使复制 class 变慢。

考虑

1) 使它们成为 def 而不是 val 因此它们不再是数据成员

2) 将它们移动到同伴 class 中以获得特征并作为 Foo.a 等访问