Array.tabulate期间的拳击双打

Boxing Double during Array.tabulate

我遇到了一个装箱问题,它对我的​​ Scala 代码的性能产生负面影响。我已经提取了相关代码,它仍然显示了问题,但增加了一些奇怪之处。我有一个 2D Double 数组的以下表示,它允许我通过提供我的函数对其执行转换:

case class Container(
  a: Array[Array[Double]] = Array.tabulate[Double](10000, 10000)((x,y) => x.toDouble * y)
) {
  def transformXY(f: (Double, Double, Double) => Double): Container = {
    Container(Array.tabulate[Double](a.length, a.length) { (x, y) =>
      f(x, y, a(x)(y))
    })
  }

  def transform(f: Double => Double): Container = {
    Container(Array.tabulate[Double](a.length, a.length) { (x, y) =>
      f(a(x)(y))
    })
  }
}

以下代码为我重现了该问题:

object Main extends App {

  def now = System.currentTimeMillis()

  val iters = 3

  def doTransformsXY() = {
    var t = Container()
    for (i <- 0 until iters) {
      val start = now
      t = t.transformXY { (x, y, h) =>
        h + math.sqrt(x * x + y * y)
      }
      println(s"transformXY: Duration ${now - start}")
    }
  }

  def doTransforms() = {
    var t = Container()
    for (i <- 0 until iters) {
      val start = now
      t = t.transform { h =>
        h + math.sqrt(h * h * h)
      }
      println(s"transform: Duration ${now - start}")
    }
  }

  if (true) { // Shows a lot of boxing if enabled
    doTransformsXY()
  }

  if (true) { // Shows a lot of boxing again - if enabled
    doTransformsXY()
  }

  if (true) { // Shows java8.JFunction...apply()
    doTransforms()
  }

  if (true) { // Shows java8.JFunction...apply() if doTransforms() is enabled
    doTransformsXY()
  }

}

当我 运行 此代码并使用 Java VisualVM 对其进行采样时,我遇到以下情况:

这是 Scala 2.12.4,Windows x64 jdk1.8.0_92

我的主要问题是关于装箱,我在生产代码中也看到了装箱:

我的第二个问题是:

why is no more boxing done once I call the transform variant?

我没有重现。如果我小心地暂停 VM 并使用 JProfiler 检查,它仍然会进行大量装箱和双打分配。这是我所期望的,我有一个解释。

查看标准库中的Function1Function2特征,我们可以看到@specialized注解:

trait Function1[@specialized(Int, Long, Float, Double) -T1, @specialized(Unit, Boolean, Int, Float, Long, Double) +R]
trait Function2[@specialized(Int, Long, Double) -T1, @specialized(Int, Long, Double) -T2, @specialized(Unit, Boolean, Int, Float, Long, Double) +R]

但是 Function3 只是

trait Function3[-T1, -T2, -T3, +R]

@specialized 是 Scala 使您避免使用原语对泛型进行装箱的方式。但这是以编译器必须生成额外的方法和 类 为代价的,因此超过某个阈值它只会产生大量可笑的代码(如果不是彻底崩溃的话)。所以 Function 有,如果我的数学是正确的,4(T1 上的规格)x 6(R 上的规格)= 每个专门方法的 24 个副本和 24 个额外的 类 除了 apply 和一个通用特征。

哦,顺便说一下,这些方法的后缀是 $mcJNI type signatures。因此,以 $mcDII 结尾的方法是 returns 一个 Double 的特殊重载,并接受两个 Int 作为参数。这是您在转换中传递给 tabulate 的函数类型,即这部分

(x, y) => f(a(x)(y))

虽然调用 f 应该显示 $mcDD 后缀(returns 一个 Double 并接受一个 Double)。

但是,调用

f(x, y, a(x)(y))

会变成

unbox(f(box(x), box(y), box(a(x)(y))))

所以我的解释已经够烦你了。是解决问题的时候了。要使两种方法的装箱具有相同的形状,请创建一个专门的接口:

trait DoubleFunction3 {
  def apply(a: Double, b: Double, c: Double): Double
}

并在 transformXY

中重写您的签名
def transformXY(f: DoubleFunction3): Container = //... same code

因为它是 Scala 2.12,而且你在 trait 中只有一个抽象方法,你仍然可以传递 lambda,所以这段代码:

  t = t.transformXY { (x, y, h) =>
    h + math.sqrt(x * x + y * y)
  }

无需更改。

现在您可能会注意到这并没有完全消除装箱,因为 tabulate 也会导致装箱。这是一维的定义 tabulate:

  def tabulate[T: ClassTag](n: Int)(f: Int => T): Array[T] = {
    val b = newBuilder[T]
    b.sizeHint(n)
    var i = 0
    while (i < n) {
      b += f(i)
      i += 1
    }
    b.result()
  }

请注意,它使用泛型 Builder[T],调用方法 +=(elem: T)Builder 本身不是专门的,因此在创建数组时会造成浪费 boxing/unboxing。您对此的解决方法是编写一个直接使用 Double 而不是 T 的版本来满足您需要的维度。