具有相同运行时间 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 作为参数传递给 seqrange 方法,因此动态调度应该调用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 的调用是单态方法调用(因为 sumRange) 并且 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.Objectsum 方法的存在并调用该方法。它可能不会被内联并且 java.lang.Integer 它 return 必须再次拆箱所以 seq 可以 return 和 int。在 range 中,编译器可以生成对 "real" sum 方法的调用,而无需对结果进行装箱和拆箱。 JVM 还可以更好地内联和优化代码。