根据我迄今为止对Racket的经验,我没有太多考虑过向量,因为我认为他们的主要好处 - 对元素的持续时间访问 - 在你使用大量元素之前并不重要。 / p>
然而,这似乎并不准确。即使使用少量元素,向量也具有性能优势。例如,分配列表比分配向量要慢:
#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
...这些列表在递归过程中是首选,因为您主要使用cons
和car
以及cdr
,它可以节省空间来处理列表(向量不能复制整个矢量,不能被打破并重新组合,对吗?)
但是在存储和检索数据元素的情况下,无论长度如何,向量似乎都具有优势。
答案 0 :(得分:18)
由于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
答案 1 :(得分:7)
在大多数编程语言中,向量与数组相同。由于任何阵列都具有固定大小,因此它们具有O(1)访问/更新。增加大小是很昂贵的,因为您需要将每个元素复制到更大尺寸的新矢量。如果你对所有元素进行循环,你可以做O(n)。
列表是单链表。它们具有动态大小,但随机访问/更新是O(n)。访问/修改列表的头部是O(1)所以如果你从头到尾迭代或从头到尾创建。由于列表迭代完成了每个步骤,因此在n个元素上的整个迭代仍然与向量一样完成O(n)。相反,执行list-ref
会使其为O(n ^ 2),因此您不会这样做。
你同时拥有列表和向量的原因是因为它们都有优点和缺点。列表是函数式编程语言的核心,因为它们可以用作不可变对象。您在每次迭代中链接一对和一对,最终得到一个大小由完整过程确定的列表。想象一下:
(define odds (filter odd? lst))
这将获取任意大小的数字列表,并创建一个包含列表中所有奇数的新列表。为了使用矢量执行此操作,您需要执行两次传递。检查结果向量应具有的大小,以及将旧元素从旧元素复制到新元素的大小。但是,如果你需要随时随机访问任何元素的向量(或哈希表,如果你用#!racket编程)是显而易见的选择。
答案 2 :(得分:5)
在你的第一个例子中:
(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时间约为20倍,实际时间约为10倍。
还有最初的#t
值。虽然我不知道Racket是否以这种方式实现它,但对于一个概念上的数组,只需要一个malloc
加一个memset
- "给我一系列内存和bitblast横跨它的这个价值。"而列表中有5000万mov
要做?
list-ref
是恕我直言,#34;代码气味" - 或者至少是我检查预期列表长度非常小的东西。如果你真的需要索引一个 big 的东西,你可能希望这个东西是一个向量(或者可能是一个哈希表)。
那么是什么列表优于矢量的优点?我认为链接列表与其他语言中的数组基本相同的优点和缺点。
此外,您可以使用cons
,car
和cdr
(例如树)在单个链接列表之外构建内容。虽然我不是Lisp历史的专家,但我想这部分是选择这些构建块的动机吗?
最后,我认为还值得记住的是,像这样的微观基准确实......就目前而言。他们不一定告诉你的是真实/完整申请中的情况。如果您的应用程序主要是分配一百万个固定长度数据结构的时间,那么您可能需要一个向量而不是列表。否则,它可能在要考虑的优化列表中相当远。
答案 3 :(得分:1)
您的问题与Racket无关;它就像任意编程语言一样:列表对矢量有什么吸引人的优势?好吧,试着想象一下如何在向量中间的某处插入元素,你就会明白。或者如何删除在向量中间找到的元素。这两个操作都是在O(1)时间内使用列表完成的,而使用矢量时,你必须移动大量元素。更重要的是,通过一些额外的工作,人们可以想出一种在恒定时间内连接两个列表(没有相同的底部元素!)的方法。唉,你不能用O(1)中的向量做到这一点(你必须分配一个足够大的新向量来保存两个操作数,然后将它们的所有元素复制到新分配的空间中)。
最后,正如上面其他人所评论的那样,对于Lisp列表而言,不仅仅是另一种数据结构;它们可以在语言的基础层找到。
所以是的,不要因为你有载体而忽视列表。清单DO有其公平的优势。