Lisp 列表是否总是在后台实现为链表?

Are Lisp lists always implemented as linked lists under the hood?

Lisp 列表是否总是在幕后实现为链表?

就处理器缓存而言,这是一个问题吗?如果是这样,是否有使用更多有助于缓存的连续结构的解决方案?

据我了解,Clojures 的序列实现为 VLists。是的,lists 通常是 Lisps 中的链表(尽管我确信有一两个实验性构建使用了其他东西)。

链接对是通常的实现方式,但过去也有其他方法。

CDR 编码是一种列表压缩方案,旨在提高某些 Lisp 机器上的硬件支持的缺点列表的连续性和数据大小。基本思想是使用标签来指示 cons 的形状:一种可能性是将下一个 cons 直接存储在第一个元素之后,基本上省略 cdr 字段。

下一个缺点本身可以​​用相同的方式压缩,因此在有利的情况下,您最终会得到一个具有出色连续性的类似数组的结构。 (它不完全是一个数组,因为必须有 space 用于标记信息,并且您不能对其进行索引。)

其中一个棘手的部分是有效地支持 carcdr 压缩内容的变异。 (参见 Steele 的论文,"Destructive Reordering of CDR-Coded Lists"。)如果 cons 是不可变的,则标记方案可以更简单。 This FAQ 对权衡进行了一些有趣的讨论。

CDR 编码的一个缺点是,由于缺点可能有多种不同 'shapes',列表操作需要在标签上分派。这引入了代码大小和分支预测错误成本。这些成本使该功能的吸引力大大降低,以至于我不知道任何使用 CDR 编码的现代 Lisp 实现。

在考虑连续性的情况下,Lisp 程序员通常会简单地使用数组。

Lisp 实现通常可以将一些值直接存储在 cons 单元中:fixnums、characters,...对于其他所有内容,指针将存储在 carcdr.[=15= 中]

现在几乎所有使用 cons 单元的实现 使用像 cdr-coding.

这样的优化

内存局部性通常通过使用复制/压缩/分代垃圾收集器来改进。

  • copying -> 当一个space满了,GC会复制list并将新的cell分配到一个新的存储区

  • 压缩 -> 一些消除内存间隙或类似的方案

  • generational -> 存活时间更长的对象会被提升到不同的存储区。因此,在一些 GC 中幸存下来的列表将被复制到另一代,并且单元格将彼此相邻分配。

有时以上 GC 策略会以奇特的方式组合。

另请注意,在许多 Lisp 程序中,许多 cons 单元可能是短暂的:

(mapcar #'1+
        (mapcar #'isqrt '(10 20 30 40 50))  ; <- result is 'garbage'
        )

整数平方根列表立即就是垃圾。该函数将只遍历新的 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)

这将摆脱一个分配列表并进一步改善局部性。

Rainer 已经提到各种内存管理技术有助于局部性。我想展示两个使用 SBCL 的实验来说明他的观点。

首先,一个快速实用程序,用于打印列表中每个 cons 的地址。

(defun print-addresses (list)
  (mapl (lambda (cons)
          (format t "address: 0x~X~%"
                  (sb-kernel:get-lisp-obj-address cons)))
        list))

在第一个实验中,我们可以看到分配是连续的,因此我们可以创建一个包含 10 个元素的列表,并查看它们的原始地址,显示它们靠得很近:

> (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,我们可以看到它已经将 cons 打包在一起:

> (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

在这些示例中,我们没有评估列表元素的位置,我想那是另一天的实验。 :-)

哲学上 "right" 的答案是 "Lisp has no lists, only CONSes"。 Conses 通常用于构建列表,以至于 CL 标准和库中的许多函数都对这些类型的列表进行操作。但是 conses 也可以用来构建其他类型的结构,比如地图或图表。所以在(传统的)Lisps 中,基本的数据结构是缺点,而不是列表。该列表只是缺点的一个方便应用。所以,"Lisp lists" 你真正的意思是 "lists implemented with Lisp conses" 而那些,好吧,不能用不同于 conses 的东西来实现;)

当然还有其他答案中提到的 CDR 编码等技术,可用于有效地表示某些基于 cons 的结构。还有一些库提供不基于链接条件的列表数据结构(如 Common Lisp 的 FSet)。

对于像 Common Lisp 和 Scheme 这样的 "traditional" Lisp 来说是这样。 Clojure 确实有列表作为基本数据类型,而 AFAIK 根本没有缺点。