在编程语言中支持布尔值和对象的表示
1 简介
本文继续讨论编程语言中值表示的进一步优化路径。在这章中,您将看到,即使是真正具有挑战性的课题-如编译器,我们凡人也可以解决,只是需要我们把手弄脏并一步一步来。
我们将在这里停止使用 OTao 语言和我们的两个解释器。我们可以持续修补它,添加新的语言功能和巧妙的速度改进。
但是,对于这个系列,我认为我们已经自然而然地完成了我们的工作。我不会重复我们在过去几页中学到的一切。你和我在一起练习后,你将记得。
大多数人可能不会在其职业生涯中花费很大一部分时间在编译器或解释器上工作。
它只是计算机科学学术界的一小部分,也是工业中软件工程的一小部分。
但是没关系。即使你一生中再也不会创建编译器,你肯定会使用它,我希望这个系统让你更好地理解你使用的编程语言是如何设计和实现的。
本文介绍使用位模式来表示nil、true、false和对象,其中nil和布尔值利用了NaN的位模式和类型标签。
对象表示利用了浮点数NaN的符号位作为类型标记,而对象指针则存储在剩余的位中。
代码示例展示了如何通过宏定义进行转换和检查。
本文的性能评估中显示,这种优化对整体VM性能的影响是分散的,可能在大型程序中更为明显。
2 在语言中表示无,真,假,Nil, true, and false
下一个要处理的类型是nil. 这非常简单,因为只有一个 nil值,因此我们只需要一个位模式来表示它。
还有另外两个单例值,两个布尔值true和false. 这需要三个总共唯一的位模式。
两个位给了我们四种不同的组合,这就足够了。
我们将未使用的尾数空间的最低两位声明为“类型标签”,以确定我们正在查看这三个单例值中的哪一个。三个类型标签的定义如下:
## value.h
#define QNAN ((uint64_t)0x7ffc000000000000)
#define TAG_NIL 1 // 01.
#define TAG_FALSE 2 // 10.
#define TAG_TRUE 3 // 11.
typedef uint64_t Value;
nil因此,我们的表示是定义我们的安静 NaN 表示所需的所有位以及nil类型标记位:
在代码中,我们像这样检查位
我们只是简单地按位或将安静的 NaN 位和类型标记,然后做一些演员舞蹈来教 C 编译器我们想要这些位的含义。
由于nil只有一位表示,我们可以在 uint64_t 上使用相等来查看 Value 是否为nil。
您可以猜到我们如何定义true和false值。
## value.h
# define AS_NUMBER(value) valueToNum(value)
#define FALSE_VAL ((Value)(uint64_t)(QNAN | TAG_FALSE))
#define TRUE_VAL ((Value)(uint64_t)(QNAN | TAG_TRUE))
#define NIL_VAL ((Value)(uint64_t)(QNAN | TAG_NIL))
这些位看起来像这样
要将 C bool 转换为 OTao 布尔值,我们依赖于这两个单例值和古老的条件运算符。
可能有一种更聪明的按位方式来做到这一点,但我的预感是编译器可以比我更快地找出一个。去另一个方向更简单。
## value.h
#define IS_NUMBER(value) (((value) & QNAN) != QNAN)
#define AS_BOOL(value) ((value) == TRUE_VAL)
#define AS_NUMBER(value) valueToNum(value)
因为我们知道 OTao 中正好有两个布尔位表示——不像在 C 中任何非零值都可以被视为“真” ——如果它不是true,它必须是false。
这个宏确实假设你只在你知道是OTao 布尔值的值上调用它。为了检查这一点,还有一个宏。
那看起来有点奇怪。一个更明显的宏看起来像这样:
#define IS_BOOL(v) ((v) == TRUE_VAL || (v) == FALSE_VAL)
不幸的是,这并不安全。扩展提到了v两次,这意味着如果该表达式有任何副作用,它们将被执行两次。
我们可以让宏调用一个单独的函数,但是,唉,真是一件苦差事。
相反,我们将 1按位OR到该值上以合并仅有的两个有效布尔位模式。这留下了价值可能处于的三个潜在状态:
1 它曾经FALSE_VAL并且现在已经转换为TRUE_VAL.
2 它是TRUE_VAL,| 1什么也没做,它仍然是TRUE_VAL。
3 它是其他一些非布尔值。
在这一点上,我们可以简单地比较结果,TRUE_VAL看看我们是处于前两种状态还是第三种状态。
3 对象的支持
最后一个值类型是最难的。与单例值不同,我们需要将数十亿个不同的指针值装入 NaN 中。
这意味着我们既需要某种标记来指示这些特定的 NaN是Obj 指针,也需要为地址本身留出空间。
我们用于单例值的标记位位于我决定存储指针本身的区域中,因此我们不能轻松地在那里使用不同的位来指示该值是一个对象引用。
但是,还有一点我们没有使用。由于我们所有的 NaN 值都不是数字——它就在名称中——符号位不用于任何东西。
我们将继续使用它作为对象的类型标记。如果我们的安静 NaN 之一设置了符号位,那么它就是一个 Obj 指针。
否则,它必须是之前的单例值之一。
即使值是 Obj 指针,我们实际上也可以使用最低位来存储类型标记。
那是因为 Obj 指针总是与 8 字节边界对齐,因为 Obj 包含 64 位字段。
反过来,这意味着 Obj 指针的三个最低位将始终为零。我们可以在那里存储我们想要的任何东西,并在取消引用指针之前将其屏蔽掉。
这是另一种称为指针标记的值表示优化。
如果设置了符号位,则剩余的低位存储指向 Obj 的指针:
要将原始 Obj 指针转换为值,我们获取指针并设置所有安静的 NaN 位和符号位。
## value.h
#define NUMBER_VAL(num) numToValue(num)
#define OBJ_VAL(obj) \
(Value)(SIGN_BIT | QNAN | (uint64_t)(uintptr_t)(obj))
static inline double valueToNum(Value value) {
指针本身是一个完整的 64 位,原则上,它可能因此与一些安静的 NaN 和符号位重叠。
但实际上,至少在我测试过的体系结构上,指针中第 48 位以上的所有内容始终为零。
这里进行了很多转换,我发现这对于满足一些最挑剔的 C 编译器是必要的,但最终结果只是将一些位卡在一起。
当谈到本书中的代码时,我试图遵循法律条文,所以这一段是可疑的。
在优化时,您不仅要突破规范所说的范围,还要突破真正的编译器和芯片让您摆脱困境的界限。
走出规范是有风险的,但在无法无天的领域也有回报。收益是否值得由您来决定。
我们像这样定义符号位:
## value.h
#ifdef NAN_BOXING
#define SIGN_BIT ((uint64_t)0x8000000000000000)
#define QNAN ((uint64_t)0x7ffc000000000000)
为了取回 Obj 指针,我们只需屏蔽掉所有这些额外的位。
## value.h
#define AS_NUMBER(value) valueToNum(value)
#define AS_OBJ(value) \
((Obj*)(uintptr_t)((value) & ~(SIGN_BIT | QNAN)))
#define BOOL_VAL(b) ((b) ? TRUE_VAL : FALSE_VAL)
波浪号 ( ~),如果您之前没有做过足够的位操作来遇到它,那么它就是按位NOT。它切换其操作数中的所有 1 和 0。
通过用安静的 NaN 和符号位的逐位否定来屏蔽该值,我们清除这些位并保留指针位。
最后一个宏:
## value.h
#define IS_NUMBER(value) (((value) & QNAN) != QNAN)
#define IS_OBJ(value) \
(((value) & (QNAN | SIGN_BIT)) == (QNAN | SIGN_BIT))
#define AS_BOOL(value) ((value) == TRUE_VAL)
- 点赞
- 收藏
- 关注作者
评论(0)