关于复制案例的表现类
On the performance of copying case classes
我有两个案例 classes:addSmall
和 addBig
。
addSmall
仅包含一个字段。
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
字段 - a
到 t
.看起来,在结果对象中,它们将被声明为常规字段,而不是 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
等访问
我有两个案例 classes:addSmall
和 addBig
。
addSmall
仅包含一个字段。
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
字段 - a
到 t
.看起来,在结果对象中,它们将被声明为常规字段,而不是 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 字段,因此将与对象一起复制。
希望对您有所帮助!
你检查过这个问题了吗?
您的 Foo
特征为每个子 class 添加 20 个成员,即使它们是常量。这将使用更多内存并使复制 class 变慢。
考虑
1) 使它们成为 def
而不是 val
因此它们不再是数据成员
或
2) 将它们移动到同伴 class 中以获得特征并作为 Foo.a
等访问