具有相同运行时间 class 但静态类型不同的对象的不同性能
Different performance of object with same runtime class but different static type
考虑以下 jmh 基准测试
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class So59893913 {
def seq(xs: Seq[Int]) = xs.sum
def range(xs: Range) = xs.sum
val xs = 1 until 100000000
@Benchmark def _seq = seq(xs)
@Benchmark def _range = range(xs)
}
给定 xs
引用相同的运行时对象 class Range.Inclusive
作为参数传递给 seq
和 range
方法,因此动态调度应该调用sum
的相同实现,尽管方法参数的声明静态类型不同,为什么性能似乎差异如此之大,如下所示?
sbt "jmh:run -i 10 -wi 5 -f 2 -t 1 -prof gc bench.So59893913"
[info] Benchmark Mode Cnt Score Error Units
[info] So59893913._range thrpt 20 334923591.408 ± 22126865.963 ops/s
[info] So59893913._range:·gc.alloc.rate thrpt 20 ≈ 10⁻⁴ MB/sec
[info] So59893913._range:·gc.alloc.rate.norm thrpt 20 ≈ 10⁻⁷ B/op
[info] So59893913._range:·gc.count thrpt 20 ≈ 0 counts
[info] So59893913._seq thrpt 20 193509091.399 ± 2347303.746 ops/s
[info] So59893913._seq:·gc.alloc.rate thrpt 20 2811.311 ± 34.142 MB/sec
[info] So59893913._seq:·gc.alloc.rate.norm thrpt 20 16.000 ± 0.001 B/op
[info] So59893913._seq:·gc.churn.PS_Eden_Space thrpt 20 2811.954 ± 33.656 MB/sec
[info] So59893913._seq:·gc.churn.PS_Eden_Space.norm thrpt 20 16.004 ± 0.035 B/op
[info] So59893913._seq:·gc.churn.PS_Survivor_Space thrpt 20 0.013 ± 0.005 MB/sec
[info] So59893913._seq:·gc.churn.PS_Survivor_Space.norm thrpt 20 ≈ 10⁻⁴ B/op
[info] So59893913._seq:·gc.count thrpt 20 3729.000 counts
[info] So59893913._seq:·gc.time thrpt 20 1864.000 ms
特别注意 gc.alloc.rate
指标的差异。
有两件事正在发生。
首先是当 xs
具有静态类型 Range
时,对 sum
的调用是单态方法调用(因为 sum
在 Range
) 并且 JVM 可以轻松地内联该方法并进一步优化它。当 xs
具有静态类型 Seq
时,它会变成一个巨型方法调用,不会得到内联和完全优化。
第二个是被调用的方法实际上并不相同。编译器在Range
中生成两个sum
方法:
scala> :javap -p scala.collection.immutable.Range
Compiled from "Range.scala"
public abstract class scala.collection.immutable.Range extends scala.collection.immutable.AbstractSeq<java.lang.Object> implements scala.collection.immutable.IndexedSeq<java.lang.Object>, scala.collection.immutable.StrictOptimizedSeqOps<java.lang.Object, scala.collection.immutable.IndexedSeq, scala.collection.immutable.IndexedSeq<java.lang.Object>>, java.io.Serializable {
...
public final <B> int sum(scala.math.Numeric<B>);
...
public final java.lang.Object sum(scala.math.Numeric);
...
}
第一个包含您在源代码中看到的实际实现。如您所见,return 是一个未装箱的 int
。第二个是这样的:
public final java.lang.Object sum(scala.math.Numeric);
Code:
0: aload_0
1: aload_1
2: invokevirtual #898 // Method sum:(Lscala/math/Numeric;)I
5: invokestatic #893 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
8: areturn
如您所见,这个只是调用另一个 sum
方法并将 int
框到 java.lang.Integer
.
因此在您的方法 seq
中,编译器只知道具有 return 类型 java.lang.Object
的 sum
方法的存在并调用该方法。它可能不会被内联并且 java.lang.Integer
它 return 必须再次拆箱所以 seq
可以 return 和 int
。在 range
中,编译器可以生成对 "real" sum
方法的调用,而无需对结果进行装箱和拆箱。 JVM 还可以更好地内联和优化代码。
考虑以下 jmh 基准测试
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class So59893913 {
def seq(xs: Seq[Int]) = xs.sum
def range(xs: Range) = xs.sum
val xs = 1 until 100000000
@Benchmark def _seq = seq(xs)
@Benchmark def _range = range(xs)
}
给定 xs
引用相同的运行时对象 class Range.Inclusive
作为参数传递给 seq
和 range
方法,因此动态调度应该调用sum
的相同实现,尽管方法参数的声明静态类型不同,为什么性能似乎差异如此之大,如下所示?
sbt "jmh:run -i 10 -wi 5 -f 2 -t 1 -prof gc bench.So59893913"
[info] Benchmark Mode Cnt Score Error Units
[info] So59893913._range thrpt 20 334923591.408 ± 22126865.963 ops/s
[info] So59893913._range:·gc.alloc.rate thrpt 20 ≈ 10⁻⁴ MB/sec
[info] So59893913._range:·gc.alloc.rate.norm thrpt 20 ≈ 10⁻⁷ B/op
[info] So59893913._range:·gc.count thrpt 20 ≈ 0 counts
[info] So59893913._seq thrpt 20 193509091.399 ± 2347303.746 ops/s
[info] So59893913._seq:·gc.alloc.rate thrpt 20 2811.311 ± 34.142 MB/sec
[info] So59893913._seq:·gc.alloc.rate.norm thrpt 20 16.000 ± 0.001 B/op
[info] So59893913._seq:·gc.churn.PS_Eden_Space thrpt 20 2811.954 ± 33.656 MB/sec
[info] So59893913._seq:·gc.churn.PS_Eden_Space.norm thrpt 20 16.004 ± 0.035 B/op
[info] So59893913._seq:·gc.churn.PS_Survivor_Space thrpt 20 0.013 ± 0.005 MB/sec
[info] So59893913._seq:·gc.churn.PS_Survivor_Space.norm thrpt 20 ≈ 10⁻⁴ B/op
[info] So59893913._seq:·gc.count thrpt 20 3729.000 counts
[info] So59893913._seq:·gc.time thrpt 20 1864.000 ms
特别注意 gc.alloc.rate
指标的差异。
有两件事正在发生。
首先是当 xs
具有静态类型 Range
时,对 sum
的调用是单态方法调用(因为 sum
在 Range
) 并且 JVM 可以轻松地内联该方法并进一步优化它。当 xs
具有静态类型 Seq
时,它会变成一个巨型方法调用,不会得到内联和完全优化。
第二个是被调用的方法实际上并不相同。编译器在Range
中生成两个sum
方法:
scala> :javap -p scala.collection.immutable.Range
Compiled from "Range.scala"
public abstract class scala.collection.immutable.Range extends scala.collection.immutable.AbstractSeq<java.lang.Object> implements scala.collection.immutable.IndexedSeq<java.lang.Object>, scala.collection.immutable.StrictOptimizedSeqOps<java.lang.Object, scala.collection.immutable.IndexedSeq, scala.collection.immutable.IndexedSeq<java.lang.Object>>, java.io.Serializable {
...
public final <B> int sum(scala.math.Numeric<B>);
...
public final java.lang.Object sum(scala.math.Numeric);
...
}
第一个包含您在源代码中看到的实际实现。如您所见,return 是一个未装箱的 int
。第二个是这样的:
public final java.lang.Object sum(scala.math.Numeric);
Code:
0: aload_0
1: aload_1
2: invokevirtual #898 // Method sum:(Lscala/math/Numeric;)I
5: invokestatic #893 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
8: areturn
如您所见,这个只是调用另一个 sum
方法并将 int
框到 java.lang.Integer
.
因此在您的方法 seq
中,编译器只知道具有 return 类型 java.lang.Object
的 sum
方法的存在并调用该方法。它可能不会被内联并且 java.lang.Integer
它 return 必须再次拆箱所以 seq
可以 return 和 int
。在 range
中,编译器可以生成对 "real" sum
方法的调用,而无需对结果进行装箱和拆箱。 JVM 还可以更好地内联和优化代码。