尝试理解Scala数组

3

我正在评估在Scala中最好的数据结构来表示稀疏向量。这些稀疏向量包含索引列表和每个索引对应的一个值。我实现了一个小型基准测试,似乎表明Array[(Long, Double)]占用的空间比2个并行数组少得多。这是正确的吗?我的基准测试是否正确?(如果我在某个地方做错了,我也不会感到惊讶)

import java.lang.management.ManagementFactory
import java.text.NumberFormat

object TestSize {

  val N = 100000000
  val formatter: NumberFormat = java.text.NumberFormat.getIntegerInstance

  def twoParallelArrays(): Unit = {

    val Z1 = Array.ofDim[Long](N)
    val Z2 = Array.ofDim[Double](N)
    Z1(N-1) = 1
    Z2(N-1) = 1.0D
    println(Z2(N-1) - Z1(N-1))
    val z1 = ManagementFactory.getMemoryMXBean.getHeapMemoryUsage.getUsed
    val z2 = ManagementFactory.getMemoryMXBean.getNonHeapMemoryUsage.getUsed
    println(s"${formatter.format(z1)} ${formatter.format(z2)}")
  }

  def arrayOfTuples(): Unit = {

    val Z = Array.ofDim[(Long, Double)](N)
    Z(N-1) = (1, 1.0D)
    println(Z(N-1)._2 - Z(N-1)._1)
    val z1 = ManagementFactory.getMemoryMXBean.getHeapMemoryUsage.getUsed
    val z2 = ManagementFactory.getMemoryMXBean.getNonHeapMemoryUsage.getUsed
    println(s"${formatter.format(z1)} ${formatter.format(z2)}")
  }

  def main(args: Array[String]): Unit = {

    // Comment one or the other to look at the results
    //arrayOfTuples()
    twoParallelArrays()
  }
}

2
关于Scala的一个重要点:名称的大小写是有意义的。因此,Z1这样的名称可能会让编译器(以及普通读者)感到困惑。除了少数例外,遵循Java命名约定。 - Bob Dalgleish
@BobDalgleish - 收到。 - Frank
1个回答

5
不,不正确。
Array.ofDim[(Long, Double)](N)

创建一个由null填充的大型数组,并且不分配任何空间给LongDouble或实际的Tuple2实例,因此在堆内存使用中看不到任何东西。
两个数组版本立即为所有LongDouble分配所有需要的空间,因此在堆内存使用中可以看到它们。
只需用适当的fill替换ofDim即可看到真正的数字。
对于大小为N = 1000000的数组:
arrayOfTuples:     45,693,312 19,190,296
twoParallelArrays: 25,925,792 19,315,256
arrayOfTuples的解决方案显然需要更多的空间。
你可能会想知道为什么系数大约为1.8而不是至少2.5。这是因为 Tuple2 是针对一些基本数据类型(特别是 LongDouble)进行了@specialized 优化,因此这两个8字节的基本数据类型可以存储在 Tuple2 中而无需装箱。因此,总开销仅为64位引用从数组到 Tuple2 的8个字节以及每个 Tuple2 实例中的一些开销。但是,仍然比直接将 LongDouble 存储在数组中要多。
顺便说一下:这正是 Apache Spark 使用所有这些 Encoder 存储数据的原因:这样您就不必担心元组和 case-class 带来的开销。
完整代码:
import java.lang.management.ManagementFactory
import java.text.NumberFormat

object TestSize {

  val N = 1000000
  val formatter: NumberFormat = java.text.NumberFormat.getIntegerInstance

  def twoParallelArrays(): Unit = {

    val Z1 = Array.fill[Long](N)(42L)
    val Z2 = Array.fill[Double](N)(42.0)
    println(Z1)
    println(Z2)
    Z1(N-1) = 1
    Z2(N-1) = 1.0D
    println(Z2(N-1) - Z1(N-1))
    val z1 = ManagementFactory.getMemoryMXBean.getHeapMemoryUsage.getUsed
    val z2 = ManagementFactory.getMemoryMXBean.getNonHeapMemoryUsage.getUsed
    Z1((new scala.util.Random).nextInt(N)) = 1234L
    Z2((new scala.util.Random).nextInt(N)) = 345.0d
    println(Z2(N-1) - Z1(N-1))
    println(s"${formatter.format(z1)} ${formatter.format(z2)}")
  }

  def arrayOfTuples(): Unit = {

    val Z = Array.fill[(Long, Double)](N)((42L, 42.0d))
    Z(N-1) = (1, 1.0D)
    println(Z(N-1)._2 - Z(N-1)._1)
    val z1 = ManagementFactory.getMemoryMXBean.getHeapMemoryUsage.getUsed
    val z2 = ManagementFactory.getMemoryMXBean.getNonHeapMemoryUsage.getUsed
    Z((new scala.util.Random).nextInt(N)) = (1234L, 543.0d)
    println(Z(N-1)._2 - Z(N-1)._1)
    println(s"${formatter.format(z1)} ${formatter.format(z2)}")
  }

  def main(args: Array[String]): Unit = {

    // Comment one or the other to look at the results
    arrayOfTuples()
    // twoParallelArrays()
  }
}

1
谢谢你,安德烈!太完美了 - 我应该初始化/填充这些数组。非常感谢。 - Frank
@Frank 看起来事后很明显,但我发现输出结果一时让人感到惊讶。同时也很高兴能够回忆起 Tuple2@specialized 的事实,所以也谢谢你的问题 :) - Andrey Tyukin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接