没有堆分配的 Scala 中的点积

Dot product in Scala without heap allocation

我有一个 Scala 项目,其中包含一些密集型算法,有时它分配浮点数的速度比 GC 清理它们的速度还快。 (这不是关于保留引用导致的内存泄漏,只是临时值的快速内存消耗。)我尝试使用原始类型的数组,并在可能的情况下重用它们,但仍然有一些新的分配潜入。

有一个让我很疑惑的,比如:

import org.specs2.mutable.Specification

class CalcTest extends Specification {

  def dot(a: Array[Float], b: Array[Float]): Float = {
    require(a.length == b.length, "array size mismatch")
    val n = a.length
    var sum: Float = 0f
    var i = 0
    while (i < n) {
      sum += a(i) * b(i)
      i += 1
    }
    sum
  }

  val vector = Array.tabulate(1000)(_.toFloat)

  "calculation" should {
    "use memory sparingly" >> {
      val before = Runtime.getRuntime().freeMemory()

      for (i <- 0 to 1000000)
        dot(vector, vector)

      val after = Runtime.getRuntime().freeMemory()
      (before - after) must be_<(1000L)  // actual result above 4M
    }
  }
}

我希望它只使用堆栈内存来计算点积,但显然它在堆上每次调用分配大约 4 个字节。这听起来可能不多,但它在我的代码中加起来很快。

我怀疑总和,但从字节码输出来看,它似乎在堆栈上:

    aload 1
    arraylength
    istore 3
    fconst_0
    fstore 4
    iconst_0
    istore 5
   l2
    iload 5
    iload 3
    if_icmpge l3
    fload 4
    aload 1
    iload 5
    faload
    aload 2
    iload 5
    faload
    fmul
    fadd
    fstore 4
    iload 5
    iconst_1
    iadd
    istore 5
    _goto l2
   l3
    fload 4
    freturn

堆上的是 return 值吗?有什么办法可以完全避免这种开销?是否有更好的方法来调查和解决此类内存问题?

从我的项目的 visualVM 输出中,我只看到我分配了非常多的浮点数。像这样的小对象很难被快速分配。它对于大对象和长时间拍摄的内存快照更有用。

更新:

我太专注于功能代码,我错过了测试中的问题。如果我用while循环重写,就成功了:

var i = 0
while (i < 1000000) {
  dot(vector, vector)
  i += 1
}

除了像这样的测试和使用 visualVM 内存快照之外,我仍然希望有更多关于调试此类问题的其他方法的想法。

中的范围实施
for (i <- 0 to 1000000)
  dot(vector, vector)

可能会使用一些内存,或者只是速度太慢,让 JVM 在后台分配其他东西并破坏测试中使用的脆弱测量方法。

尝试将这些行修改为 while 循环,例如。

(这个post原来的版本说for()等价于map(),这是错误的,这里等价于foreach(),因为它没有yield子句。)