值类型
(Value Types)
值类型是(非递归的)数据类型,它们不在堆上分配,而是作为值在栈上传递。由于所有数据类型都是不可变的,从语义上讲这些类型就是等价的,但值类型可能更高效,因为它们避免了堆分配和引用计数(或者也可能更昂贵,因为它们需要复制而不是共享引用)。
默认情况下,任何大小最多为 3 个机器字(= 64 位平台上的 24 字节)的非递归归纳数据类型都被视为值类型。例如,二元组和三元组按值传递和返回。通常,这意味着在进行优化编译时,此类元组将返回到寄存器中。
3 个机器字是一个平衡点:在这个尺寸以下,复制成本通常低于引用计数开销,且有机会放进寄存器;超过这个尺寸,引用计数方案开始占优。
我们还可以通过在 type 或 struct 声明前使用 value 关键字,来强制将某个类型编译为值类型。
高级
装箱
为了支持泛型多态,值类型有时需要装箱(boxing)。list<a> 只有一份定义、编译成一种表示:无论 a 被实例化成什么,列表节点的元素槽都必须保持固定大小。普通元素(如 int)可以直接填入槽中,但元组这类值类型的内联表示随类型不同、维数不同而大小不一,放不进固定槽——于是元素只能装箱(boxed)、在堆上分配,列表节点的元素槽里存放的是指向元组的统一大小的指针。构造元组列表 [(1,True)] 时,编译器内部就把元素 (1,True) 转换为 [Box((1,True))]。
对常规数据类型和 int 而言,装箱是“免费“的:装箱表示与未装箱表示在数学上同构(一一对应、可逆无损),且编译器能在优化阶段消除不必要的装箱。此外,不超过 63 位(64 位平台上)的值类型采用就地装箱(in-place boxing),无需堆分配(如 int32)。float64 也有专门的底层表示优化:默认情况下,只有绝对值超出 2⁻⁵¹¹ 到 2⁵¹² 范围(无穷大与 NaN 除外)的 float64 才会堆分配。
对于性能敏感的代码,我们可以针对特定元素类型“特化“多态数据类型,以减少装箱带来的分配。例如:
type mylist
MyCons{ head1: int; head2: bool; mytail: mylist }
MyNil
这里把 list<(int,bool)> 特化成一个专门的递归类型:head1、head2 直接作为 MyCons 的内联字段存储,不再需要把元组装箱。于是前面 [(1,True)] 的例子对应写成 MyCons(1,True,MyNil):原来每个元素需要两次分配(Cons 节点一次、堆上装箱的元组一次),现在只需为 MyCons 分配一次,还省掉了指向元组的间接引用。未来 Koka 开发组希望扩展编译器,自动执行这种特化,或通过特殊指令实现。