我最近潜入Scala并且(可能是可以预见的)花了不少时间研究Scala标准库中的不可变集合API。
我正在编写一个应用程序,它必须在大型集合上执行许多+/-操作。出于这个原因,我想确保我选择的实现是一个所谓的“持久”数据结构,这样我就可以避免进行写时复制。我看到了Martin Odersky的this answer,但它并没有真正为我解决这个问题。
我编写了以下测试代码来比较ListSet和HashSet对添加操作的性能:
import scala.collection.immutable._
object TestListSet extends App {
var set = new ListSet[Int]
for(i <- 0 to 100000) {
set += i
}
}
object TestHashSet extends App {
var set = new HashSet[Int]
for(i <- 0 to 100000) {
set += i
}
}
以下是HashSet的粗略运行时测量:
$ time scala TestHashSet
real 0m0.955s
user 0m1.192s
sys 0m0.147s
和ListSet:
$ time scala TestListSet
real 0m30.516s
user 0m30.612s
sys 0m0.168s
单链表上的缺点是恒定时间操作,但这种性能看起来线性或更差。这个性能是否与需要检查集合中的每个元素以确保对象相等以符合Set的无重复不变量有关?如果是这种情况,我意识到这与“持久性”无关。
至于官方文档,我所能找到的只是以下页面,但似乎不完整:Scala 2.8 Collections API -- Performance Characteristics。由于ListSet最初似乎是其内存占用的一个很好的选择,因此可能应该在API文档中提供有关其性能的一些信息。
答案 0 :(得分:9)
一个古老的问题,但也是在错误的基础上得出结论的一个很好的例子。
Connor,基本上你正在尝试做一个微基准测试。那是一般不推荐和该死的正确地做。
为什么呢?因为JVM除了执行示例中的代码之外还执行许多其他操作。它是加载类,进行垃圾收集,将字节码编译为本机代码等等。所有这些都是动态的,并且基于在运行时采样的不同指标。
所以你不能用上面的测试代码来概括两个集合的性能。例如,您实际可能正在测量的可能是+=
HashSet
方法的编译时间和ListSet
的垃圾收集时间。所以这是苹果和梨之间的比较。
要正确地进行微观基准测试,您应该:
+=
方法)。-XX:-PrintCompilation
和-XX:-PrintGC
)。如果在测试期间运行,则丢弃结果。我建议您阅读Brian Goetz撰写的Oracle's recommendations for doing micro benchmarks和一篇关于benchmark pitfalls的精彩文章。
另外,如果你想使用一个好的工具,为你完成上述所有工作,请尝试Google的Caliper。
答案 1 :(得分:8)
来自ListSet
来源的关键行是(在子类Node
内):
override def +(e: A): ListSet[A] = if (contains(e)) this else new Node(e)
您可以看到只有在尚未包含项目时才添加该项目。因此添加到集合中的是O(n)
。您通常可以假设XMap具有与XSet类似的性能特征,ListMap
一直列为线性时间。这就是原因,也就是集合的表现方式。
P.S。在TestHashSet案例中,您正在测量启动时间。它的速度提高了30多倍。
答案 2 :(得分:5)
由于集合必须没有重复项,因此在添加元素之前,Set必须检查它是否已包含该元素。在无法保证元素位置的列表中搜索将是O(N)线性时间。同样的一般想法适用于其删除操作。
使用HashSet,该类定义了一个函数,该函数为O(1)中的任何元素选取一个位置,这使得contains(element)方法更快,代价是占用更多空间以减少元素的机会位置碰撞。