Common Lisp 中的对象内存布局

IgS*_*lov 2 memory sbcl common-lisp

我知道 Common Lisp 不鼓励程序员接触原始内存,但我想知道是否可以查看对象如何在字节级别存储。当然,垃圾收集器在内存空间中移动对象,并且函数的两次后续调用(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
Run Code Online (Sandbox Code Playgroud)

咱们试试吧:

(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)
Run Code Online (Sandbox Code Playgroud)

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

这段代码演示了这一点:

(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)
Run Code Online (Sandbox Code Playgroud)

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

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

ign*_*ens 6

正如我在评论中提到的,内存中对象的布局是非常特定于实现的,并且探索它的工具也必然依赖于实现。

此答案讨论 SBCL 的 64 位版本的布局,并且仅讨论具有“宽修复数”的 64 位版本。我不确定这两件事以什么顺序到达 SBCL,因为早在 SBCL 和 CMUCL 分歧之前我就没有认真看过这些。

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

下面的信息来自查看GitHub 镜像,它似乎与规范源非常最新,但速度要快得多。

指针、直接对象、标签

[信息来自此处。] SBCL 在两个字边界上分配。在 64 位系统上,这意味着任何地址的低四位始终为零。这些低四位用作标签(文档称其为“lowtag”)来告诉您单词的其余部分是什么类型的内容。

  • xyz 0的低标记意味着该字的其余部分是固定数,特别是xyz将是固定数的低位,而不是标记位。这意味着有 63 位可用于修复编号,并且修复编号的添加是微不足道的:您不需要屏蔽任何位。
  • xy 01的低标签意味着单词的其余部分是其他一些直接对象。lowtag 右侧的一些位(我认为 SBCL 称之为“widetag”,尽管我对此感到困惑,因为该术语似乎有两种使用方式)将说明直接对象是什么。直接对象的示例是字符和单浮点数(在 64 位平台上!)。
  • 其余的 lowtag 模式是xy 11,它们都意味着事物是指向某个非直接对象的指针:
  • 0011 是某物的一个实例;
  • 0111 是一个缺点;
  • 第1011章
  • 1111是另外一回事。

缺点

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

其他非直接对象

我认为(但不确定)所有其他非直接对象都有一个词来说明它们是什么(也可以称为“宽标签”)和至少一个其他词(因为分配是在两个词的边界上) 。我怀疑函数的特殊标记意味着函数调用只能跳转到函数代码的入口点。

看着这个

room.lisp有一个很好的函数叫做hexdump它知道如何打印出非立即对象。基于此,我写了一个小垫片(如下),试图告诉您有用的事情。这里有些例子。

> (hexdump-thing 1)
lowtags:    0010
fixnum:     0000000000000002 = 1
Run Code Online (Sandbox Code Playgroud)

1是一个固定数,其表示形式如上所述仅右移一位。请注意,在这种情况下,lowtags 实际上包含整个值!

> (hexdump-thing 85757)
lowtags:    1010
fixnum:     0000000000029DFA = 85757
Run Code Online (Sandbox Code Playgroud)

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

> (hexdump-thing #\c)
lowtags:    1001
immediate:  0000000000006349 = #\c
> (hexdump-thing 1.0s0)
lowtags:    1001
immediate:  3F80000000000019 = 1.0
Run Code Online (Sandbox Code Playgroud)

字符和单个浮点数是直接的:我认为 lowtag 左侧的一些位告诉系统它们是什么?

> (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)
Run Code Online (Sandbox Code Playgroud)

缺点。在第一种情况下,您可以看到两个固定数字作为立即值位于 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
Run Code Online (Sandbox Code Playgroud)

字符串。它们具有一些类型信息、长度字段,然后将字符打包为两个单词。单字符字符串需要四个单词,与四字符字符串相同。您可以从数据中读取字符代码。

> (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
Run Code Online (Sandbox Code Playgroud)

对于简单向量来说也是如此:标题、长度,但现在每个条目当然都需要一个单词。以上所有条目都是固定数字,您可以在数据中看到它们。

所以它继续。


执行此操作的代码

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

> (hexdump-thing 85757)
lowtags:    1010
fixnum:     0000000000029DFA = 85757
Run Code Online (Sandbox Code Playgroud)