不变性和内存使用

Immutability and memory usage

我原以为在向不可变 Seq 添加元素后,内存使用量的增加非常小。因为新引用 (val) 可以重新使用为已创建的 val 分配的内存,并且只需要为新元素分配额外的内存。换句话说,我认为没有必要创建防御副本(参见此处https://users.scala-lang.org/t/inquiry-on-immutabillity-of-objects-and-how-they-affect-memory/2340)。

然而,实际上,添加一个元素后,内存使用量增加到与创建全新 Seq 相同的数量。以下是 SBT 项目的代码片段

object obj extends App {

  val n = 1000000

  def usedMem = {
    val runtime = Runtime.getRuntime
    runtime.totalMemory - runtime.freeMemory
  }

  val m1 = usedMem
  
  val ar = scala.collection.mutable.ArrayBuffer.fill(n)("hohohohoho")
  val m2 = usedMem
  println ("1 " + (m2-m1))

  val sq = Seq.fill(n)("hohohohoho")
  val m3 = usedMem
  println ("2 " + (m3-m2))

  val ar0 = ar :+ "yo"
  val m4 = usedMem
  println ("3 " + (m4-m3))

  val sq0 = sq :+ "yo"
  val m5 = usedMem
  println ("4 " + (m5-m4))

}

在输出中,我们看到第 2 步和第 4 步之后的内存使用量增加了近 24MB。不过,我预计在第 4 步之后会有非常小的增长。

1 4103816
2 23948288
3 4546184
4 23948600

有人可以解释这种行为吗?或者如何看到内存被重新用于不可变对象。

此外,我很惊讶 ArrayBuffer 的内存使用量比 Seq 小得多(4MB 对 24MB)。也能理解这种差异会很棒。

那是因为您没有为作业使用正确的数据结构。

这里:val sq = Seq.fill(n)("hohohohoho") 你正在创建一个 SeqSeq 是一个抽象类型,因此它必须选择一个具体的实现。截至撰写本文之日,此类默认值通常为 List

并且Lists在附加时不能共享内存,他们必须复制所有内容(这意味着它也很慢)

现在,您可以做的是使用经过优化的追加数据结构,例如 Vectorcats Chain
您也可以直接使用 List 并预先添加而不是附加,例如:

val sq = List.fill(n)("hohohohoho")
val sq0 = "yo" :: sq

这是我个人不喜欢Seq的原因之一,它没有提供足够的信息来做任何有用的事情