Array.tabulate

时间:2018-02-07 15:38:31

标签: arrays scala performance jvm boxing

我遇到了一个拳击问题,这会影响我的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对其进行采样时,我会遇到以下情况:

  • doTransformsXY正在运行时,我发现scala.runtime.BoxesRunTime.boxToDouble()
  • 花了很多时间
  • 运行doTransforms后,没有更多时间用于装箱,示例显示scala.runtime.java8.JFunction2$mcDII$sp.apply()
  • 我再次运行doTransformsXY,仍然没有明显的拳击,scala.runtime.java8.JFunction2$mcDII$sp.apply()
  • 的时间再次增长

这是使用Scala 2.12.4,Windows x64 jdk1.8.0_92

我的主要问题是拳击,我在生产代码中也看到了:

  • Double为什么发生Array.tabulate拳击?我是否需要进行程序化(循环,手动Array创建)以避免它?

我的第二个问题是:

  • 为什么我打电话给transform变种后不再进行拳击?

1 个答案:

答案 0 :(得分:1)

  

为什么一旦我调用变换变体就不再进行拳击了?

我没有重现那个。如果我小心地暂停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结尾的方法是一个返回Double的专用重载,并接受两个Ints作为参数。这是您在变换中传入tabulate的函数类型,即此部分

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

虽然对f的调用应显示$mcDD后缀(返回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中只有一个抽象方法,你仍然可以传递lambdas,所以这段代码:

  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本身并不专业,因此在创建阵列时会造成浪费的装箱/拆箱。您对此的解决方法是编写一个直接使用Double而不是T的版本,以获取所需的维度。