为什么在 OCaml 中 int 只有31位?

在其他地方没见过这个“特色”。我知道第32位用于垃圾收集。但是为什么只有 int 类型是这样,而其他基本类型不是这样呢?

31917 次浏览

It's not exactly "used for garbage collection." It's used for internally distinguishing between a pointer and an unboxed integer.

See the the "representation of integers, tag bits, heap-allocated values" section of https://ocaml.org/learn/tutorials/performance_and_profiling.html for a good description.

The short answer is that it is for performance. When passing an argument to a function it is either passed as an integer or a pointer. At a machine level language level there is no way to tell if a register contains an integer or a pointer, it is just a 32 or 64 bit value. So the OCaml run time checks the tag bit to determine if what it received was an integer or a pointer. If the tag bit is set, then the value is an integer and it is passed to the correct overload. Otherwise it is a pointer and type is looked up.

为什么只有整数有这个标签?因为其他所有内容都作为指针传递。传递的是一个整数或指向其他数据类型的指针。只有一个标签位,就只能有两种情况。

这就是所谓的 tagged pointer表示,是一个非常常见的优化技巧,在许多不同的解释器,虚拟机和运行时系统中使用了几十年。几乎每个 Lisp 实现都使用它们,许多 Smalltalk VM、许多 Ruby 解释器等等。

通常,在这些语言中,您总是传递指向对象的指针。对象本身包含一个对象头,其中包含对象元数据(如对象的类型、类、可能的访问控制限制或安全注释等) ,然后是实际的对象数据本身。因此,一个简单的整数将被表示为一个指针加上一个由元数据和实际整数组成的对象。即使使用非常紧凑的表示形式,对于一个简单的整数,也需要6字节。

另外,您不能将这样的整数对象传递给 CPU 来执行快速整数算术。如果要添加两个整数,则 真的只有两个指针,它们指向要添加的两个整数对象的对象标题的开头。因此,首先需要对第一个指针执行整数运算,将偏移量添加到存储整数数据的对象中。那你就得取消对那个地址的引用。对第二个整数再次执行相同操作。现在您有两个整数,您实际上可以要求 CPU 添加。当然,现在需要构造一个新的整数对象来保存结果。

因此,为了执行 整数加法,实际上需要执行 整数加法和两个指针解引用以及一个对象构造。你占用了将近20字节。

然而,诀窍在于,对于所谓的 不可变的值类型(比如整数) ,你通常不会在对象头中包含所有的元数据: 你可以省略掉所有的东西,只是简单地合成它(这是 VM-nerd 的说法,意思是“伪造它”) ,当有人想看的时候。一个整数将 一直都是有类 Integer,没有必要单独存储该信息。如果有人使用反射算出一个整数的类,你只需要回复 Integer,没有人会知道你实际上没有在对象头中存储这些信息,事实上,不是甚至有一个对象头(或一个对象)。

所以,诀窍是将值 这个对象存储在指针 这个对象中,有效地将两者合二为一。

有些 CPU 实际上在指针(所谓的 标签)中有额外的空间,允许您在指针本身中存储有关指针的额外信息。额外的信息,比如“这实际上不是一个指针,这是一个整数”。例如 Burroughs B5000、各种 Lisp 机器或 AS/400。不幸的是,当前大多数主流 CPU 都没有这个特性。

然而,有一个解决办法: 当地址没有在字边界上对齐时,大多数当前主流 CPU 的工作速度都会显著降低。有些甚至根本不支持未对齐访问。

这意味着在实践中,所有指针将被4整除,这意味着它们将以两个 0位结束。这使我们能够区分 真的指针(以 00结尾)和实际上是伪装的整数(以 1结尾)的指针。它仍然给我们留下了所有以 10结尾的指针,让我们可以自由地做其他事情。此外,大多数现代操作系统为自己保留了非常低的地址,这为我们提供了另一个混乱的领域(指针,以24个 0开始,以 00结束)。

因此,您可以将一个31位整数编码到一个指针中,只需将其向左移动1位并将 1添加到该指针中即可。您可以对它们执行 非常快整数算法,只需适当地移动它们(有时甚至不需要这样做)。

What do we do with those other address spaces? Well, typical examples include encoding floats in the other large address space and a number of special objects like true, false, nil, the 127 ASCII characters, some commonly used short strings, the empty list, the empty object, the empty array and so on near the 0 address.

例如,在 MRI、 YARV 和 Rubinius Ruby 解释器中,整数按照我上面描述的方式进行编码,false被编码为地址 0(恰好 还有false在 C 中的表示) ,true被编码为地址 2(恰好 true的 C 表示移动了一位) ,nil被编码为 4

我必须添加这个链接,以帮助 OP 了解更多的 A 63-bit floating-point type for 64-bit OCaml

虽然这篇文章的标题看起来是关于 float的,但实际上是关于 extra 1 bit

OCaml 运行时允许通过统一 每个 OCaml 值表示为一个单独的 这样就有可能只有一个实现,例如, “ list of things”,带有要访问的函数(例如 List.length)和 构建(例如 List.map)这些列表,它们的工作方式是否相同 是整数列表、浮点数列表或整数集列表。

Anything that does not fit in in a word is allocated in a block in the 表示该数据的单词就是指向该块的指针。 由于堆只包含单词块,所以所有这些指针都是 对齐: 它们的少数最低有效位总是未设置。

无参数构造函数(如下所示: type water = Apple | Orange | 香蕉)和整数不能表示如此多的信息,以至于它们 需要在堆中分配。它们的表示形式是未装箱的 数据直接位于单词的内部,否则这个单词就会是 因此,当一个列表列表实际上是一个指针列表时,一个 整型数列表包含少了一个间接的整型数 访问和构建列表的函数不会注意到,因为 int 和 指针的大小相同。

不过,垃圾收集者需要 able to recognize pointers from integers. A pointer points to a 定义为活的堆中的格式良好的块(因为它是 being visited by the GC) and should be marked so. An integer can have 如果不采取预防措施,可能会意外地看到 像一个指针。这可能导致死块看起来是活的,但很多 worse, it would also cause the GC to change bits in what it thinks is 活动块的头部,当它实际上跟随一个整数时 看起来像个指针,搞乱了用户数据。

这就是为什么未装箱的整数提供31位(对于32位 OCaml)或63位(对于 64-bit OCaml) to the OCaml programmer. In the representation, behind 场景,包含整数的单词的最低有效位 总是设置为31位或63位,以便与指针区分 整数是相当不寻常的,所以任何使用 OCaml 的人都知道 OCaml 的用户通常不知道的是为什么没有一个 用于64位 OCaml 的63位非装箱浮动类型。

为什么在 OCaml 中 int 只有31位?

基本上,为了在主要操作为模式匹配,主要数据类型为变量类型的情况下,获得最好的性能。最佳的数据表示形式是使用标签来区分指针和未装箱数据的统一表示形式。

但是为什么只有 int 类型是这样,而其他基本类型不是这样呢?

不仅 int。其他类型,如 char和枚举使用相同的标记表示。