在 Racket 中,列表相对于向量的优势是啥?

Posted

技术标签:

【中文标题】在 Racket 中,列表相对于向量的优势是啥?【英文标题】:In Racket, what is the advantage of lists over vectors?在 Racket 中,列表相对于向量的优势是什么? 【发布时间】:2015-02-19 11:22:09 【问题描述】:

根据我目前使用 Racket 的经验,我没有过多考虑向量,因为我发现它们的主要好处——对元素的恒定时间访问——在你使用大量元素之前并不重要。

但是,这似乎不太准确。即使元素数量很少,向量也具有性能优势。例如,分配一个列表比分配一个向量要慢:

#lang racket

(time (for ([i (in-range 1000000)]) (make-list 50 #t)))
(time (for ([i (in-range 1000000)]) (make-vector 50 #t)))

>cpu time: 1337 real time: 1346 gc time: 987
>cpu time: 123 real time: 124 gc time: 39

而且检索元素也更慢:

#lang racket

(define l (range 50))
(define v (make-vector 50 0))

(time (for ([i (in-range 1000000)]) (list-ref l 49)))
(time (for ([i (in-range 1000000)]) (vector-ref v 49)))

>cpu time: 77 real time: 76 gc time: 0
>cpu time: 15 real time: 15 gc time: 0

顺便说一句,如果我们增加到 1000 万,这个性能比仍然成立:

#lang racket

(define l (range 50))
(define v (make-vector 50 0))

(time (for ([i (in-range 10000000)]) (list-ref l 49)))
(time (for ([i (in-range 10000000)]) (vector-ref v 49)))

>cpu time: 710 real time: 709 gc time: 0
>cpu time: 116 real time: 116 gc time: 0

当然,这些是综合示例,大多数程序不会在循环中分配结构或使用list-ref 一百万次。 (是的,我故意抓住第 50 个元素来说明性能差异。)

但它们也不是,因为在依赖列表的整个程序中,每次触摸这些列表时都会产生一些额外的开销,所有这些低效率的小问题都会加起来导致运行时间变慢为整个计划。

因此我的问题是:为什么不一直使用向量?在什么情况下,我们应该期望列表有更好的性能?

我最好的猜测是因为从列表的前面中取出一个项目同样快,例如:

#lang racket

(define l (range 50))
(define v (make-vector 50 0))

(time (for ([i (in-range 1000000)]) (list-ref l 0)))
(time (for ([i (in-range 1000000)]) (vector-ref v 0)))

>cpu time: 15 real time: 16 gc time: 0
>cpu time: 12 real time: 11 gc time: 0

...列表在递归情况下是首选,因为您主要使用conscarcdr,并且它节省了使用列表的空间(向量不能被破坏并放回一起而不复制整个向量,对吧?)

但在您存储和检索数据元素的情况下,无论长度如何,向量似乎都有优势。

【问题讨论】:

我认为我不介意使用list-ref。查找不是线性的。 何时使用(从更一般的意义上)数组与链表? 我很确定,尽管这是一个 C++ 视频,但它在这里解释了问题:youtube.com/watch?v=YQs6IC-vgmo 请注意,长度也需要线性时间,因此如果您想单独测量 list-ref,请将 (length l) 移到 for 循环之外。 @MatthewButterick:在 Lisp 和 Scheme 中,列表只是一个单链表。我想不出 Lisp 或 Scheme 比任何其他语言有什么实质性的好处。我知道 Clojure 做事不同。我怀疑那里的差异会比传统实现小得多。 【参考方案1】:

由于list-ref 使用与索引成线性关系的时间,除非用于短列表,否则很少使用。但是,如果访问模式是顺序的并且元素的数量可以变化,那么列表就可以了。看到一个对 50 个元素长的 fixnums 列表的元素求和的基准会很有趣。

但对数据结构的访问模式并不总是顺序的。

这是我如何选择在 Racket 中使用的数据结构:

DATA STRUCTURE   ACCESS       NUMBER     INDICES
List:            sequential   Variable   not used
Struct:          random       Fixed      names
Vector:          random       Fixed      integer
Growable vector: random       Variable   integer
Hash:            random       Variable   hashable
Splay:           random       Variable   non-integer, total order

【讨论】:

很清楚。此表以及惯用示例应在 Racket 文档中。 顺便说一下,虽然其中一些数据结构在"Datatypes" 中进行了描述,但其他数据结构在"Data: Data Structures"(data 模块)中进行了描述。例如vector 位于前一部分,而gvector(可增长向量)位于后者。 研究论文 “Functional Data Structures in Typed Racket” 基于 reference implementations 讨论了在某些情况下性能优于标准列表的替代函数数据结构。 @GregHendershott 感谢您分享这些信息。哈,有趣的是,不知何故,在我对 Racket 文档的所有访问中,我从未找到 gvector 的东西。现在查看docs.racket-lang.org/guide/index.html 和docs.racket-lang.org/reference/index.html 的TOC,我看到“数据:数据结构”弹出。我认为这可能是一个可见性问题。【参考方案2】:

向量与大多数编程语言中的数组相同。与任何数组一样,它们具有固定大小,它们具有 O(1) 访问/更新。增加尺寸是昂贵的,因为您需要将每个元素复制到更大尺寸的新向量中。如果你对所有元素进行循环,你可以做到 O(n)。

列表是单链表。它们具有动态大小,但随机访问/更新是 O(n)。访问/修改列表的头部是 O(1) 所以如果你从头到尾迭代或从头到尾创建。由于列表迭代每一步都完成了对 n 个元素的整个迭代,因此仍然像向量一样完成 O(n)。改为 list-ref 会使它变成 O(n^2),所以你不会。

您同时拥有列表和向量的原因是因为它们都有优点和缺点。列表是函数式编程语言的核心,因为它们可以用作不可变对象。您在每次迭代中链接一对和一对,最终得到一个列表,其大小由完整过程确定。想象一下:

(define odds (filter odd? lst)) 

这需要一个任意大小的数字列表,并创建一个包含列表中所有奇数的新列表。为了使用矢量执行此操作,您需要执行两次传递。一种检查结果向量应具有的大小,另一种将每个奇数元素从旧元素复制到新元素。但是,如果您需要随时随机访问任何元素,则向量(或哈希表,如果您在 #!racket 中编程)是显而易见的选择。

【讨论】:

【参考方案3】:

在你的第一个例子中:

(time (for ([i (in-range 1000000)]) (make-list   50 #t))) ;50 million list nodes
(time (for ([i (in-range 1000000)]) (make-vector 50 #t))) ; 1 million vectors

请记住,您要求对列表进行 50 倍分配。 GC时间~20x,real time~10x其实还不错。

还有初始的#t 值。虽然我不知道 Racket 是否以这种方式实现它,但对于概念上只需要一个 malloc 加上一个 memset 的数组——“给我一个内存范围,并在其中对这个值进行位爆破。”而有一个 5000 万 movs 的列表呢?

list-ref 恕我直言是“代码气味”——或者,至少,我会检查预期列表长度是否会非常小。如果你真的需要索引一个 big 的东西,你可能希望那个东西是一个向量(或者可能是一个哈希表)。

那么列表相对于向量的优势是什么?我认为与其他语言的数组相比,链表基本上具有相同的优点和缺点。

您还可以使用conscarcdr 构建单链表以外的内容(例如树)。虽然我不是 Lisp 历史方面的专家,但我想这部分是我选择这些构建块的动机?

最后,我认为还值得牢记的是,像这样的微基准测试是真实的……就目前而言。他们不一定告诉您的是真实/完整应用程序中的情况。如果您的应用程序被分配一百万个固定长度数据结构的时间所支配,那么您可能确实需要一个向量而不是一个列表。否则,它可能远远超出要考虑的优化列表。

【讨论】:

【参考方案4】:

您的问题与 Racket 无关;它代表任意编程语言:列表相对于向量有哪些引人注目的优势?好吧,试着想象一下如何在向量中间的某个地方插入一个元素,你就会明白了。或者如何删除向量中间的元素。对于列表,这两个操作都在 O(1) 时间内完成,而对于向量,您必须移动大量元素。更重要的是,通过一些额外的工作,人们可以想出一种在恒定时间内加入两个列表(没有相同的底部元素!)的方法。唉,你不能用 O(1) 中的向量来做到这一点(你必须分配一个足够大的新向量来容纳两个操作数,然后将它们的所有元素复制到新分配的空间中)。

最后,正如上面其他人评论的那样,对于 Lisp,列表不仅仅是另一种数据结构;它们位于语言的最基础层。

所以是的,不要仅仅因为你有向量就忽略列表。列表确实有其应有的优势。

【讨论】:

以上是关于在 Racket 中,列表相对于向量的优势是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 中,流相对于循环的优势是啥? [关闭]

T-trees 相对于 B+/-trees 的优势是啥?

在 Scheme / Racket 中 let 的 lambda 定义是啥? [复制]

为啥要使用thumb模式,与ARM相比较,Thumb代码的两大优势是啥?

聚合类相对于常规类的优势[重复]

Singleton类的优势是啥? [复制]