考虑以下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
引用作为参数传递给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
的调用是单态方法调用,JVM可以轻松地内联该方法并对其进行进一步优化。当xs
的静态类型为Seq
时,它将变成一个大形方法调用,不会内联和完全优化。
第二个是被调用的方法是实际上不是相同的。编译器在sum
:中生成two
Range
方法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);
...
}
第一个包含您在源代码中看到的实际实现。如您所见,它返回一个未装箱的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
中,编译器仅知道具有返回类型sum
的java.lang.Object
方法的存在并调用该方法。它可能不会内联,并且返回的java.lang.Integer
必须重新装箱,以便seq
可以返回int
。在range
中,编译器可以生成对“真实” sum
方法的调用,而无需对结果进行装箱和拆箱。 JVM在内联和优化代码方面也可以做得更好。