【问题标题】:When should I choose Vector in Scala?什么时候应该在 Scala 中选择 Vector?
【发布时间】:2011-08-03 14:45:07
【问题描述】:

看来Vector Scala 收藏派对迟到了,所有有影响力的博文都已经离开了。

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

【问题讨论】:

  • 我想我在这里的意思是,在 Java 中我会创建写 List<String> l = new ArrayList<String>() Scala 博客会让你相信每个人都使用 List 来获得持久的集合优点 - 但 Vector 足够通用我们应该在 List 的地方使用它吗?
  • @Debilski:我想知道你的意思是什么。当我在 REPL 输入 Seq() 时,我得到一个 List
  • 嗯,好吧,文档中是这么说的。也许这仅适用于IndexedSeq
  • 关于Seq 的默认具体类型的评论已超过三年。从 Scala 2.11.4(及更早版本)开始,Seq 的默认具体类型为 List
  • 对于随机访问,vector 更好。对于头部、尾部访问,列表更好。对于批量操作,如 map、filter、vector 是首选,因为 vector 由 32 个元素组成一个块,而 list 用指针组织元素,不能保证这些元素彼此接近。

标签: scala vector scala-collections


【解决方案1】:

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

  • head 的更新比 List 慢(虽然没有你想象的那么快)

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

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

如果我有一个List[A] 类型的序列,我实际上是在断言两件事。首先,我的算法(和数据)完全是堆栈结构的。其次,我断言我要对这个集合做的唯一事情就是完整的 O(n) 遍历。这两个真的是齐头并进。相反,如果我有 Vector[A] 类型的东西,我要断言的唯一是我的数据具有明确定义的顺序和有限长度。因此,Vector 的断言较弱,这导致其更大的灵活性。

【讨论】:

  • 2.10 已经出了一段时间了,List 模式匹配还是比 Vector 好吗?
  • 列表模式匹配不再好。事实上,情况恰恰相反。例如,要获得头部和尾部,可以使用case head +: tailcase tail :+ head。要匹配空,你可以做case Seq()等等。您需要的一切都在 API 中,比List 的更通用
  • List 是通过单链表实现的。 Vector 的实现类似于 Java 的 ArrayList
  • @JosiahYoder 它的实现与 ArrayList 完全不同。 ArrayList 包装了一个动态调整大小的数组。 Vector 是一个trie,其中的键是值的索引。
  • 我很抱歉。我正在访问一个对细节模糊不清的网络资源。我应该更正我之前的陈述吗?还是那种不好的形式?
【解决方案2】:

好吧,如果算法可以单独使用::headtail 来实现,那么List 可以非常快。最近我有一个客观的教训,当我通过生成 List 而不是 Array 来击败 Java 的 split,并且无法用其他任何东西击败它。

但是,List 有一个基本问题:它不适用于并行算法。我无法以有效的方式将 List 拆分为多个段,或将其连接回来。

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

因此,考虑到所有因素,Vector 是最佳选择除非您有特定的考虑,使其他集合之一更可取 - 例如,如果您愿意,您可以选择 Stream惰性求值和缓存(Iterator 更快,但不缓存),或者List,如果算法是通过我提到的操作自然实现的。

顺便说一句,最好使用SeqIndexedSeq,除非您需要特定的API(例如List::),甚至GenSeqGenIndexedSeq if您的算法可以并行运行。

【讨论】:

  • 感谢您的回答。您所说的“有很好的地方性”是什么意思?
  • @ngocdaothanh 这意味着数据在内存中紧密组合在一起,提高了数据在需要时进入缓存的机会。
  • @user247077 是的,考虑到我提到的细节,列表在性能上可以击败向量。并不是向量的所有动作都被摊销 O(1)。事实上,在不可变数据结构上(就是这种情况),任一端的交替插入/删除根本不会摊销。在这种情况下,缓存是无用的,因为您总是在复制向量。
  • @user247077 也许你不知道Vector 是Scala 中的不可变数据结构?
  • @user247077 它比这更复杂,包括一些内部可变的东西以使附加更便宜,但是当你将它用作堆栈时,这是不可变列表的最佳方案,你最终仍然拥有相同的链表的内存特性,但具有更大的内存分配配置文件。
【解决方案3】:

这里的一些陈述令人困惑甚至是错误的,尤其是 Scala 中的 immutable.Vector 类似于 ArrayList 的想法。 List 和 Vector 都是不可变的、持久的(即“获得修改后的副本很便宜”)数据结构。 没有合理的默认选择,因为它们可能适用于可变数据结构,但这取决于您的算法在做什么。 List 是一个单链表,而 Vector 是一个 base-32 整数 trie,即它是一种具有 32 度节点的搜索树。 使用这种结构,Vector 可以相当快地提供最常见的操作,即在 O(log_32(n)) 中。这适用于头/尾中的前置、附加、更新、随机访问、分解。顺序迭代是线性的。 另一方面,列表仅提供线性迭代和恒定时间前置,头/尾分解。其他一切都需要一般的线性时间。

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

那么我们应该使用哪种数据结构呢? 基本上有四种常见的情况:

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

【讨论】:

    【解决方案4】:

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

    对于IndexedSeq,您通常会选择VectorRanges 和 WrappedStrings 也是 IndexedSeqs。

    对于LinearSeq,您通常会选择List 或其惰性等效项Stream。其他示例是Queues 和Stacks。

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

    【讨论】:

    • 但是如果 Vector 的迭代速度比 List 的快,而且我也可以映射 fold 等,那么除了一些特殊情况(基本上所有专门用于 List 的 FP 算法)之外,似乎 List 本质上是遗产。
    • @Duncan 您从哪里听说 Vector 的迭代速度更快?首先,您需要跟踪和更新当前索引,而链表不需要这样做。我不会将列表函数称为“特殊情况”——它们是函数式编程的基础。不使用它们就像尝试在没有 for 或 while 循环的情况下编写 Java。
    • 我很确定Vector 的迭代更快,但需要有人对其进行基准测试才能确定。
    • 我认为Vector 中的 (?) 元素以 32 个一组的形式物理存在于 RAM 中,它们更完全适合 CPU 缓存...因此缓存未命中率更低
    【解决方案5】:

    在涉及大量随机访问和随机突变的情况下,Vector(或 - 如docs 所说 - Seq)似乎是一个很好的折衷方案。这也是performance characteristics 所建议的。

    此外,Vector 类似乎在分布式环境中运行良好,没有太多数据重复,因为不需要对完整对象进行写时复制。 (见:http://akka.io/docs/akka/1.1.3/scala/stm.html#persistent-datastructures

    【讨论】:

    • 要学的东西太多了... Vector 作为默认 Seq 是什么意思?如果我写 Seq(1, 2, 3) 我得到 List[Int] 而不是 Vector[Int]。
    • 如果您有随机访问权限,请使用IndexedSeq。这也是Vector,但这是另一回事。
    • @DuncanMcGregor:向量是默认的IndexedSeq,它实现了SeqSeq(1, 2, 3) 是使用 List 实现的 LinearSeq
    【解决方案6】:

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

    如果您不需要不可变的数据结构,请坚持使用 ArrayBuffer,因为它是 Scala 等价于 ArrayList。

    【讨论】:

    • 我坚持不可变的、持久的集合领域。我的观点是,即使我不需要随机访问,Vector 是否有效地取代了 List?
    • 取决于用例。向量更加平衡。迭代比列表快,随机访问要快得多。更新速度较慢,因为它不仅仅是一个列表前置,除非它是可以使用构建器完成的折叠的批量更新。也就是说,我认为 Vector 是最好的默认选择,因为它用途广泛。
    • 我认为这是我问题的核心 - 向量非常好,我们不妨在示例通常显示列表的地方使用它们。
    猜你喜欢
    • 1970-01-01
    • 2015-12-12
    • 2015-05-19
    • 2023-04-10
    • 2016-03-12
    • 2011-11-23
    • 2010-12-30
    • 2017-07-24
    相关资源
    最近更新 更多