【问题标题】:Are Lisp lists always implemented as linked lists under the hood?Lisp 列表是否总是在底层实现为链表?
【发布时间】:2015-05-17 04:05:28
【问题描述】:

Lisp 列表是否总是在底层实现为链表?

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

【问题讨论】:

    标签: linked-list lisp cpu-cache


    【解决方案1】:

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

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

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

    其中一个棘手的部分是有效地支持压缩 conses 的 carcdr 的突变。 (参见 Steele 的论文,“Destructive Reordering of CDR-Coded Lists”。)如果 conses 是不可变的,则标记方案可以更简单。 This FAQ 有一些关于权衡的有趣讨论。

    CDR 编码的一个缺点是,因为 cons 可以是各种不同的“形状”,所以列表操作需要在标签上调度。这引入了代码大小和分支错误预测成本。这些成本大大降低了该功能的吸引力,以至于我不知道任何使用 CDR 编码的现代 Lisp 实现。

    如果需要考虑连续性,Lisp 程序员通常会简单地使用数组。

    【讨论】:

      【解决方案2】:

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

      现在几乎所有使用 cons 单元的实现使用像 cdr-coding 这样的优化。

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

      • copying -> 当空间已满时,GC 会复制列表并将新的单元格分配到新的存储区域中,彼此相邻

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

      • 世代 -> 寿命较长的对象将被提升到不同的存储区域。因此,在一些 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)
      

      这将摆脱一个分配的列表并进一步提高局部性。

      【讨论】:

        【解决方案3】:

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

        首先,一个快速实用程序可以打印列表中每个缺点的地址。

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

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

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

        【讨论】:

          【解决方案4】:

          哲学上“正确”的答案是“Lisp 没有列表,只有缺点”。 Conses 通常用于构建列表,以至于 CL 标准和库中的许多函数都在这些类型的列表上运行。但是 conses 也可以用于构建其他类型的结构,例如地图或图形。所以在(传统的)Lisps 中,基本的数据结构是 cons,而不是列表。该列表只是缺点的一种方便应用。因此,“Lisp 列表”实际上是指“使用 Lisp conses 实现的列表”,而这些列表不能用与 conses 不同的东西来实现;)

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

          对于像 Common Lisp 和 Scheme 这样的“传统”Lisp 来说,这是正确的。 Clojure 确实将列表作为基本数据类型,而 AFAIK 则根本没有 conses。

          【讨论】:

          • 我猜他们应该将语言命名为 CONP。
          【解决方案5】:

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-10-30
            • 1970-01-01
            • 1970-01-01
            • 2011-09-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多