Lisp列表是否总是作为链接列表实现?
就处理器缓存而言,这是一个问题吗?如果是这样,是否存在使用更多连续结构来帮助缓存的解决方案?
答案 0 :(得分:6)
链接对是通常的实现,但过去还有其他方法。
CDR编码是一种列表压缩方案,旨在改善某些Lisp机器上硬件支持的cons列表的连续性和数据大小。基本思想是使用标签来表示缺点的形状:其中一种可能性是直接在第一个元素之后存储下一个缺点,基本上忽略了cdr
字段。
下一个缺点本身可以以相同的方式进行压缩,因此在有利的情况下,您最终会得到一个具有极佳邻接的阵列式结构。 (它不是一个数组,因为标记信息必须有空格,你不能将其编入索引。)
其中一个棘手的部分是有效支持压缩后的car
和cdr
突变。 (参见Steele的论文," CDR编码列表的破坏性重新排序"。)如果conses是不可变的,则标记方案可以更简单。 This FAQ对权衡进行了一些有趣的讨论。
CDR编码的缺点是,由于缺点可能是各种不同的形状,因此列表操作需要在标签上进行调度。这引入了代码大小和分支错误预测成本。这些成本使得该功能的吸引力大大降低,以至于我不知道任何使用CDR编码的现代Lisp实现。
如果需要考虑邻接,Lisp程序员通常只会使用数组。
答案 1 :(得分:4)
Lisp实现通常可以直接在cons单元格中存储一些值:fixnums,characters,......对于其他所有内容,指针将存储在car
或cdr
中。
现在几乎所有使用cons单元 的实现都不会使用像cdr-coding
这样的优化。
通常使用复制/压缩/分代垃圾收集器来改进内存局部性。
复制 - >当空间已满时,GC会复制列表并在新存储区域中将新单元格分配到彼此
压缩 - >一些摆脱记忆差距或类似的方案
世代 - >较长的生物物体将被提升到不同的存储区域。因此,在某些GC中幸存下来的列表将被复制到另一代,并且这些单元格将彼此相邻分配。
有时,上面的GC连续性会以奇特的方式结合起来。
另请注意,在许多Lisp程序中,许多缺陷单元可能是短暂的:
(mapcar #'1+
(mapcar #'isqrt '(10 20 30 40 50)) ; <- result is 'garbage'
)
整数平方根列表立即是垃圾。该函数将遍历新鲜的cons细胞并分配新的cons cons细胞,并且不会有很多缓存非局部性。
通过使用破坏性操作可以减少cons单元的分配。以上可以写成:
CL-USER 24 > (let ((s (mapcar #'isqrt '(10 20 30 40 50))))
(map-into s #'1+ s))
(4 5 6 7 8)
这将删除一个已分配的列表并进一步改善位置。
答案 2 :(得分:3)
Rainer已经提到各种内存管理技术有助于本地化。我想用SBCL展示两个实验来说明他的观点。
首先,快速实用程序在列表中打印每个缺点的地址。
(defun print-addresses (list)
(mapl (lambda (cons)
(format t "address: 0x~X~%"
(sb-kernel:get-lisp-obj-address cons)))
list))
在第一个实验中,我们可以看到分配是连续的,因此我们可以创建一个包含十个元素的列表,并且在它们的原始地址处查看它们会显示它们一起关闭:
> (print-addresses (loop repeat 10 collect 'dummy))
address: 0x1003F57167
address: 0x1003F57177
address: 0x1003F57187
address: 0x1003F57197
address: 0x1003F571A7
address: 0x1003F571B7
address: 0x1003F571C7
address: 0x1003F571D7
address: 0x1003F571E7
address: 0x1003F571F7
第二次实验。如果我们之间进行一些不相关的分配怎么办?让我们将这样一个列表分配给一个变量,以便我们以后可以戳它。
(defparameter *another-list*
(loop repeat 10
;; using eval to trick the compiler into
;; compiling this piece of dummy code
do (eval '(make-array (random 1000)))
collect 'dummy))
我们可以看到这次地址更随机:
> (print-addresses *another-list*)
address: 0x10046E9AF7
address: 0x10046EB367
address: 0x10046ECB97
address: 0x10046EE827
address: 0x10046EF247
address: 0x10046F1F17
address: 0x10046F2007
address: 0x10046F3FD7
address: 0x10046F5E67
address: 0x10046F6887
现在,如果我们使用(sb-ext:gc)
调用GC,我们可以看到它已将所有内容打包在一起:
> (sb-ext:gc)
> (print-addresses *another-list*)
address: 0x1004738007
address: 0x1004738017
address: 0x1004738027
address: 0x1004738037
address: 0x1004738047
address: 0x1004738057
address: 0x1004738067
address: 0x1004738077
address: 0x1004738087
address: 0x1004738097
在这些例子中,我们没有评估列表元素的位置,我想这是另一天的实验。 : - )
答案 3 :(得分:3)
当然,在其他答案中提到的像CDR编码这样的技术可以用来有效地表示某些基于cons的结构。还有一些库提供了不基于链接的conses的列表数据结构(如Common Lisp的FSet)。
对于#34;传统&#34; Lisp,如Common Lisp和Scheme。 Clojure确实将列表作为基本数据类型,AFAIK根本没有说明。
答案 4 :(得分:1)
据我了解,Clojures&#39;序列实现为VLists。是的,list
通常是Lisp中的链接列表(尽管我确定这是一个或两个使用其他东西的实验性构建)。