Common Lisp 中的对象内存布局

Object memory layout in Common Lisp

我知道 Common Lisp 不鼓励程序员接触原始内存,但我想知道是否可以查看对象是如何在字节级别存储的。当然,垃圾收集器在内存中移动对象 space 并且随后两次调用函数 (obj-as-bytes obj) 可能会产生不同的结果,但让我们假设我们只需要一个内存快照。你会如何实现这样的功能?

我对 SBCL 的尝试如下所示:

(defun obj-as-bytes (obj)
  (let* ((addr (sb-kernel:get-lisp-obj-address obj)) ;; get obj address in memory
         (ptr (sb-sys:int-sap addr))                 ;; make pointer to this area
         (size (sb-ext:primitive-object-size obj))   ;; get object size 
         (output))
    (dotimes (idx size)
      (push (sb-sys:sap-ref-64 ptr idx) output))     ;; collect raw bytes into list
    (nreverse output)))                              ;; return bytes in the reversed order

让我们试试:

(obj-as-bytes #(1)) =>
(0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 111 40 161 4 16 0 0 0 23 1 16 80 0 0 0)

(obj-as-bytes #(2) =>
(0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 95 66 161 4 16 0 0 0 23 1 16 80 0 0 0)

从这个输出我得出结论,有很多垃圾,这些垃圾占用 space 用于未来的内存分配。我们看到它是因为 (sb-ext:primitive-object-size obj) 似乎 return 一块足以容纳对象的内存。

这段代码演示了它:

(loop for n from 0 below 64 collect
  (sb-ext:primitive-object-size (make-string n :initial-element #\a))) =>
(16 32 32 32 32 48 48 48 48 64 64 64 64 80 80 80 80 96 96 96 96 112 112 112 112                                                                                                                               128 128 128 128 144 144 144 144 160 160 160 160 176 176 176 176 192 192 192                                                                                                                                     192 208 208 208 208 224 224 224 224 240 240 240 240 256 256 256 256 272 272                                                                                                                                     272)

因此,如果 sb-ext:primitive-object-size 更准确,obj-as-bytes 会给出正确的结果。但是我找不到任何替代方法。

对于如何修复此功能或如何以不同方式实现它,您有什么建议吗?

正如我在评论中提到的 object 在内存中的布局非常 implementation-specific 并且探索它的工具也必然是 implementation-dependent.

此答案讨论了 64 位版本 SBCL 的布局,并且仅适用于具有 'wide fixnums' 的 64 位版本。我不确定这两件事是按什么顺序到达 SBCL 的,因为在 SBCL 和 CMUCL 分歧之前我就没有认真看过这些。

这个答案也可能是错误的:我不是 SBCL 开发人员,我只是添加它,因为没有人有(我怀疑正确标记问题可能帮助解决这个问题)。

以下信息来自查看 the GitHub mirror, which seems to be very up to date with the canonical source,但速度要快得多。

指针,即时 objects,标签

[来自 here 的信息] SBCL 在 two-word 边界上分配。在 64 位系统上,这意味着任何地址的低四位始终为零。这些低四位用作标记(文档称之为 'lowtag')来告诉您单词其余部分是什么东西。

  • xyz0 的 lowtag 意味着单词的其余部分是一个 fixnum,特别是 xyz 将是低位fixnum 的位,而不是标记位。这意味着有 63 位可用于 fixnums 并且 fixnum 添加是微不足道的:您不需要屏蔽掉任何位。
  • xy01 的低标记意味着该词的其余部分是其他直接 object。 lowtag 右侧的一些位(我认为 SBCL 称之为 'widetag' 虽然我对此感到困惑,因为该术语似乎以两种方式使用)会说明立即数 object 是什么.立即数 object 的示例是字符和 single-floats(在 64 位平台上!)。
  • 剩下的lowtag模式是xy11,它们都表示事物是指向某些non-immediate object:
  • 的指针
  • 0011 是某物的实例;
  • 0111 是缺点;
  • 1011 是函数;
  • 1111 是另外一回事。

结论

因为 cons 不需要任何额外的类型信息(一个 cons 就是一个 cons)lowtag 就足够了:一个 cons 就是内存中的两个单词,每个单词依次有 lowtags &c。

其他non-immediateobjects

我认为(但我不确定)所有其他 non-immediate object 都有一个词来说明它们是什么(也可以称为 'widetag')并且在至少一个其他词(因为分配是在 two-word 边界上)。我怀疑函数的特殊标记意味着函数调用可以直接跳转到函数代码的入口点。

看着这个

room.lisp 有一个很好的函数叫做 hexdump,它知道如何打印出 non-immediate objects。基于此,我写了一个小垫片(如下),它试图告诉你有用的东西。这里有一些例子。

> (hexdump-thing 1)
lowtags:    0010
fixnum:     0000000000000002 = 1

1 是一个 fixnum,它的表示只是像上面描述的那样右移一位。请注意,在这种情况下,lowtags 实际上包含整个值!

> (hexdump-thing 85757)
lowtags:    1010
fixnum:     0000000000029DFA = 85757

...但在这种情况下不是。

> (hexdump-thing #\c)
lowtags:    1001
immediate:  0000000000006349 = #\c
> (hexdump-thing 1.0s0)
lowtags:    1001
immediate:  3F80000000000019 = 1.0

字符和单个浮点数是立即数:低位标签左侧的一些位告诉系统它们是什么,我想?

> (hexdump-thing '(1 . 2))
lowtags:    0111
cons:       00000010024D6E07 : 00000010024D6E00
10024D6E00: 0000000000000002 = 1
10024D6E08: 0000000000000004 = 2
> (hexdump-thing '(1 2 3))
lowtags:    0111
cons:       00000010024E4BC7 : 00000010024E4BC0
10024E4BC0: 0000000000000002 = 1
10024E4BC8: 00000010024E4BD7 = (2 3)

同意。在第一种情况下,您可以看到两个 fixnums 在 cons 的两个字段中作为立即值。在第二个中,如果你解码第二个字段的低标签,它将是 0111:这是另一个缺点。

> (hexdump-thing "")
lowtags:    1111
other:      00000010024FAE8F : 00000010024FAE80
10024FAE80: 00000000000000E5
10024FAE88: 0000000000000000 = 0
> (hexdump-thing "x")
lowtags:    1111
other:      00000010024FC22F : 00000010024FC220
10024FC220: 00000000000000E5
10024FC228: 0000000000000002 = 1
10024FC230: 0000000000000078 = 60
10024FC238: 0000000000000000 = 0
> (hexdump-thing "xyzt")
lowtags:    1111
other:      00000010024FDDAF : 00000010024FDDA0
10024FDDA0: 00000000000000E5
10024FDDA8: 0000000000000008 = 4
10024FDDB0: 0000007900000078 = 259845521468
10024FDDB8: 000000740000007A = 249108103229

字符串。这些有一些类型信息,一个长度字段,然后字符被打包成两个单词。 single-character 字符串需要四个单词,与 four-character 字符串相同。您可以从数据中读取字符代码。

> (hexdump-thing #())
lowtags:    1111
other:      0000001002511C3F : 0000001002511C30
1002511C30: 0000000000000089
1002511C38: 0000000000000000 = 0
> (hexdump-thing #(1))
lowtags:    1111
other:      00000010025152BF : 00000010025152B0
10025152B0: 0000000000000089
10025152B8: 0000000000000002 = 1
10025152C0: 0000000000000002 = 1
10025152C8: 0000000000000000 = 0
> (hexdump-thing #(1 2))
lowtags:    1111
other:      000000100252DC2F : 000000100252DC20
100252DC20: 0000000000000089
100252DC28: 0000000000000004 = 2
100252DC30: 0000000000000002 = 1
100252DC38: 0000000000000004 = 2
> (hexdump-thing #(1 2 3))
lowtags:    1111
other:      0000001002531C8F : 0000001002531C80
1002531C80: 0000000000000089
1002531C88: 0000000000000006 = 3
1002531C90: 0000000000000002 = 1
1002531C98: 0000000000000004 = 2
1002531CA0: 0000000000000006 = 3
1002531CA8: 0000000000000000 = 0

简单向量的处理相同:header,长度,但现在每个条目当然需要一个词。以上所有条目都是fixnums,你可以在数据中看到它们。

这样下去。


执行此操作的代码

这个可能是错误的而且它的早期版本肯定不喜欢小的大数字(我认为hexdump不喜欢它们)。如果您想要真正的答案,请阅读源代码或询问 SBCL 人员。其他实现可用,并且会有所不同。

(defun hexdump-thing (obj)
  ;; Try and hexdump an object, including immediate objects.  All the
  ;; work is done by sb-vm:hexdump in the interesting cases.
  #-(and SBCL 64-bit)
  (error "not a 64-bit SBCL")
  (let* ((address/thing (sb-kernel:get-lisp-obj-address obj))
         (tags (ldb (byte 4 0) address/thing)))
    (format t "~&lowtags: ~12T~4,'0b~%" tags)
    (cond
      ((zerop (ldb (byte 1 0) tags))
       (format t "~&fixnum:~12T~16,'0x = ~S~%" address/thing obj))
      ((= (ldb (byte 2 0) tags) #b01)
       (format t "~&immediate:~12T~16,'0x = ~S~%" address/thing obj))
      ((= (ldb (byte 2 0) tags) #b11)   ;must be true
       (format t "~&~A:~12T~16,'0x : ~16,'0x~%"
               (case (ldb (byte 2 2) tags)
                 (#b00 "instance")
                 (#b01 "cons")
                 (#b10 "function")
                 (#b11 "other"))
               address/thing (dpb #b0000 (byte 4 0) address/thing))
       ;; this tells you at least something (and really annoyingly
       ;; does not pad addresses on the left)
       (sb-vm:hexdump obj))
      ;; can't happen
      (t (error "mutant"))))
  (values))