在Scala中什么时候应该选择Vector



看来Vector在Scala集合聚会上迟到了,所有有影响力的博客文章都已经离开了。

在Java中ArrayList是默认的集合-我可能会使用LinkedList,但只有当我已经考虑过算法并足够关注优化时。在Scala中,我应该使用Vector作为我的默认Seq,还是试图找出List实际上更合适的时候?

作为一般规则,默认使用Vector。对于几乎所有内容,它都比List快,对于大于一般大小的序列,它的内存效率更高。请参阅有关Vector与其他集合的相对性能的文档。使用Vector也有一些缺点。具体来说:

  • 头部的更新比List慢(尽管没有你想象的那么多)

Scala 2.10之前的另一个缺点是模式匹配对List的支持更好,但在2.10中使用通用的+::+提取器纠正了这一点。

还有一种更抽象的代数方法来解决这个问题:在概念上具有什么样的序列?另外,从概念上来说,用它做什么?如果我看到一个函数返回一个Option[A],我知道这个函数在它的域中有一些漏洞(因此是局部的)。我们可以将同样的逻辑应用于集合。

如果我有一个类型为List[A]的序列,我有效地断言了两件事。首先,我的算法(和数据)完全是堆栈结构的。其次,我断言我要对这个集合做的唯一事情是满的O(n)遍历。这两者是密切相关的。相反,如果我有类型为Vector[A]的东西,则我断言的only内容是我的数据具有定义良好的顺序和有限的长度。因此,Vector的断言更弱,这导致了它更大的灵活性。

嗯,如果算法可以单独使用::, headtail来实现,那么List可以非常快。我最近就有过这样的经验,当我通过生成List而不是Array来击败Java的split时,其他任何东西都无法击败它。

然而,List有一个基本问题:它不能与并行算法一起工作。我不能以一种有效的方式将List分割成多个段,或者将其连接起来。

还有其他类型的集合可以更好地处理并行性,Vector就是其中之一。Vector也有很好的局部性——List没有——这对一些算法来说是一个真正的优势。

因此,考虑到所有因素,Vector是最佳选择,除非您有特定的考虑,使其他集合中的一个更可取——例如,如果您想要延迟计算和缓存(Iterator更快,但不缓存),您可以选择Stream,或者如果算法自然地实现了我提到的操作,您可以选择List

顺便说一下,最好使用SeqIndexedSeq,除非你想要一个特定的API(如List::),甚至GenSeqGenIndexedSeq,如果你的算法可以并行运行。

这里的一些语句令人困惑,甚至是错误的,特别是不可变的概念。Scala中的Vector类似于ArrayList。List和Vector都是不可变的、持久的(即:"获得修改后的副本很便宜")数据结构。对于可变数据结构,没有合理的默认选择,但这取决于你的算法在做什么。List是一个单链表,而Vector是一个以32为基数的整数树,即它是一种节点度为32的搜索树。使用这种结构,Vector可以相当快地提供大多数常见操作,即在O(log_32(n))内。这适用于前置,追加,更新,随机访问,头/尾分解。顺序迭代是线性的。另一方面,列表只提供线性迭代和常数时间前置,头/尾分解。其他的都是线性时间

这看起来好像Vector几乎在所有情况下都可以很好地替代List,但是在函数式程序中,prepend、分解和迭代通常是对序列的关键操作,由于Vector的结构更复杂,这些操作的常量(要高得多)。我做了一些测量,所以迭代速度大约是列表的两倍,prepend速度大约是列表的100倍,head/tail分解速度大约是列表的10倍,可遍历生成速度大约是向量的2倍。(这可能是因为使用构建器构建Vector时,Vector可以一次分配32个元素的数组,而不是逐个添加或添加元素)。当然,所有在列表上花费线性时间而在向量上花费常数时间的操作(如随机访问或追加)在大列表上将会非常慢。

那么我们应该使用哪种数据结构呢?基本上,有四种常见的情况:
  • 我们只需要通过map, filter, fold等操作来变换序列:基本上,这无关紧要,我们应该对我们的算法进行通用编程,甚至可能从接受并行序列中受益。对于顺序操作,List可能要快一些。但是如果你必须优化的话,你应该对它进行基准测试。
  • 我们需要大量的随机访问和不同的更新,所以我们应该使用向量,列表将会非常慢。
  • 我们以经典的函数方式操作列表,通过前置和递归分解迭代来构建它们:使用list, vector将慢10-100倍或更多。
  • 我们有一个性能关键的算法,基本上是命令式的,在列表上做了很多随机访问,有点像就地快速排序:使用命令式数据结构,例如ArrayBuffer,在本地复制数据。

对于不可变集合,如果您想要一个序列,您的主要决定是使用IndexedSeq还是LinearSeq,它们提供不同的性能保证。IndexedSeq提供元素的快速随机访问和快速长度操作。LinearSeq只通过head提供对第一个元素的快速访问,但也有一个快速的tail操作。(摘自Seq文档)

对于IndexedSeq,通常会选择VectorRange s和WrappedString s也是IndexedSeqs。

对于LinearSeq,您通常会选择List或其惰性等效Stream。其他的例子是Queue s和Stack s。

所以在Java术语中,ArrayList与Scala的Vector使用相似,LinkedList与Scala的List使用相似。但在Scala中,我更倾向于使用List而不是Vector,因为Scala对包括遍历序列的函数有更好的支持,比如映射、折叠、迭代等。您将倾向于使用这些函数来操作整个列表,而不是随机访问单个元素。

在涉及大量随机访问和随机突变的情况下,Vector(或者-正如文档所说- Seq)似乎是一个很好的折衷方案。这也是性能特征所表明的。

而且,Vector类似乎在没有大量数据复制的分布式环境中发挥得很好,因为不需要对整个对象进行写时复制。(参见:http://akka.io/docs/akka/1.1.3/scala/stm.html#persistent-datastructures)

如果您正在进行不可变编程,并且需要随机访问,那么Seq是最好的选择(除非您想要一个Set,而实际上您经常这样做)。除此之外,List工作得很好,只是它的操作不能并行化。

如果你不需要不可变的数据结构,坚持使用ArrayBuffer,因为它相当于Scala中的ArrayList。

最新更新