Performance Scala:可变与不可变对象性能-OutOfMemoryError

Performance Scala:可变与不可变对象性能-OutOfMemoryError,performance,memory,scala,garbage-collection,mutability,Performance,Memory,Scala,Garbage Collection,Mutability,我想比较Scala中immutable.Map和mutable.Map的性能特征,以获得类似的操作(即,将多个映射合并为一个映射。请参阅)。对于可变映射和不可变映射,我都有类似的实现(见下文) 作为测试,我生成了一个包含1000000个单项映射[Int,Int]的列表,并将该列表传递到我正在测试的函数中。有了足够的内存,结果就不足为奇了:~1200ms用于mutable.Map,1800ms用于immutable.Map,750ms用于使用mutable.Map的命令式实现——不确定是什么原因造

我想比较Scala中immutable.Map和mutable.Map的性能特征,以获得类似的操作(即,将多个映射合并为一个映射。请参阅)。对于可变映射和不可变映射,我都有类似的实现(见下文)

作为测试,我生成了一个包含1000000个单项映射[Int,Int]的列表,并将该列表传递到我正在测试的函数中。有了足够的内存,结果就不足为奇了:~1200ms用于mutable.Map,1800ms用于immutable.Map,750ms用于使用mutable.Map的命令式实现——不确定是什么原因造成了巨大的差异,但也请随意评论

让我有点吃惊的是,可能是因为我有点笨,在IntelliJ 8.1中使用默认运行配置时,两个可变实现都会遇到OutOfMemoryError,但不可变集合没有。不可变测试确实运行到完成,但速度非常慢,大约需要28秒。当我增加最大JVM内存(大约200MB,不确定阈值在哪里)时,我得到了上面的结果

不管怎样,以下是我真正想知道的:

为什么可变实现会耗尽内存,但是不可变的实现不允许?我怀疑不可变的版本允许垃圾收集器在可变的实现之前运行并释放内存——所有这些垃圾收集都解释了不可变的低内存运行的缓慢性——但是我想要一个更详细的解释

实现如下。(注意:我并不认为这些是可能的最佳实现。请随时提出改进建议。)

def mergeMaps[A,B](func:(B,B)=>B)(listofmap:List[Map[A,B]]):Map[A,B]=
(图[A,B]()/:(用于(m func(acc(kv.\U 1),kv.\U 2)或kv)
}
def mergeMutableMaps[A,B](func:(B,B)=>B)(listofmap:List[mutable.Map[A,B]]):mutable.Map[A,B]=
(可变映射[A,B]()/:(对于(m func(acc(kv.\u 1),kv.\u 2)其他kv)
}
def mergemutable祈使命令[A,B](func:(B,B)=>B)(listofmap:List[mutable.Map[A,B]]):mutable.Map[A,B]={
val toReturn=mutable.Map[A,B]()

对于(m嗯,这实际上取决于您使用的映射的实际类型。可能是
HashMap
。现在,像这样的可变结构通过预分配预期使用的内存来获得性能。您将加入一百万个映射,因此最终的映射肯定会有点大。让我们看看如何添加这些键/值:

protected def addEntry(e: Entry) { 
  val h = index(elemHashCode(e.key)) 
  e.next = table(h).asInstanceOf[Entry] 
  table(h) = e 
  tableSize = tableSize + 1 
  if (tableSize > threshold) 
    resize(2 * table.length) 
} 
请参见
resize
行中的
2*
?可变的
HashMap
在每次空间用完时都会加倍增长,而不可变的在内存使用方面非常保守(尽管现有键在更新时通常会占用两倍的空间)

现在,对于其他性能问题,您正在前两个版本中创建一个键和值列表。这意味着,在加入任何映射之前,您已经拥有了每个
Tuple2
(键/值对)内存中两次!加上
列表的开销,这是很小的,但我们谈论的是超过一百万个元素的开销

您可能希望使用投影,这样可以避免这种情况。不幸的是,投影是基于
流的
,这对于我们在Scala 2.7.x上的目的来说不是很可靠。不过,请尝试以下方法:

for (m <- listOfMaps.projection; kv <- m) yield kv
现在,让我们看看前两个算法中使用的代码(减去
mutable
关键字):

在这种情况下,您不会获得任何东西。将
分配给
kvs
后,数据尚未被复制。但是,一旦执行第二行,kvs将计算其每个元素,因此将保留数据的完整副本

现在考虑原始形式::

(Map[A,B]() /: (for (m <- listOfMaps.projection; kv <-m) yield kv)) 
如果
为空,只需返回累加器。否则,计算一个新累加器(
f(z,head)
),然后将其和函数传递给
流的
尾部

但是,一旦执行了
f(z,head)
,就不会有对
head
的剩余引用。或者,换句话说,程序中的任何地方都不会指向
流的
head
,这意味着垃圾收集器可以收集它,从而释放内存

最终的结果是,当你用它来计算累加器时,由for comprehension生成的每个元素都将短暂地存在。这就是保存整个数据副本的方法


最后,还有一个问题,为什么第三种算法不能从中受益。好吧,第三种算法没有使用
产量
,因此没有任何数据的复制。在这种情况下,使用
投影
只会添加一个间接层。

很有趣。我按照你的建议,将列表切换到流,而re并没有提高性能。事实上,命令式实现在所需时间上增加了一倍。但是,可变实现不再耗尽内存。此外,也许这是一个无意义的问题,但前两个版本如何创建键和值列表?这是因为for()吗屈服理解创建了一个新列表?我不清楚在for表达式中调用.projection()如何避免这种情况。这是因为理解返回的序列类型与传递的序列类型相同吗?我将补充我的答案,因为这太多了,无法作为注释保留。好的,我补充了我的答案。简短的是,“这是关于记忆,而不是表现”,“是的,这是因为对于理解”,“是的,这是因为对于理解,返回的是它传递的相同类型的o序列。”,尽管最后一个答案不是真的。每个集合,或作为第一个生成器传递给
for
的任何内容,都可能返回它想要的任何内容。碰巧
Listval l = List(1,2,3)
val l2 = for (e <- l) yield e*2
(Map[A,B]() /: (for (m <- listOfMaps; kv <-m) yield kv)) 
val kvs = for (m <- listOfMaps.projection; kv <-m) yield kv
(Map[A,B]() /: kvs) { ... }
(Map[A,B]() /: (for (m <- listOfMaps.projection; kv <-m) yield kv)) 
override final def foldLeft[B](z: B)(f: (B, A) => B): B = { 
  if (isEmpty) z 
  else tail.foldLeft(f(z, head))(f) 
}