为探索erl内部的tuple和list的构造和内部实现,我们可以从list_to_tuple/1这个erlang的bif函数说起。
先看看$ERL_TOP/erts/emulator/beam/bif.c文件中list_to_tuple/1函数的C源码。
为了便于说明,对源码做了少量修改,并增加了一些变量打印。
list_to_tuple/1的内部实现
BIF_RETTYPE list_to_tuple_1(BIF_ALIST_1)
{
// 接收参数,它是一个指向list内容的指针值
// 注意,这是一个被封装过的地址值,不能直接当作系统指针使用
Eterm list = BIF_ARG_1;
Eterm* cons;
Eterm res;
Eterm* hp;
int len;
// 计算list的长度
if ((len = list_length(list)) < 0 || len > ERTS_MAX_TUPLE_SIZE) {
BIF_ERROR(BIF_P, BADARG);
}
// 给将要创建的tuple分配内存空间
// 从这一句可以看出,tuple的长度为list长度+1
// 为什么要+1?
// 因为每个tuple的头部都有个header,
// 用来存放arityval,即tuple的长度
hp = HAlloc(BIF_P, len+1);
// 将系统指针封装成erl数据类型
res = make_tuple(hp);
// 生成tuple的arityval(header)
int arityval = make_arityval(len);
// 为了观察变量值,我增加了下面这一句
erts_fprintf(stderr,
"list:%X hp:%p res:%X arityval:%X\n",
list, hp, res, arityval);
// 将header存入tuple的首4字节
*hp++ = arityval;
// is_list(list)内部是如何判断它是list?
// 这个问题可参见《Erlang数据类型的内部实现》一文
// http://blog.csdn.net/u011471961/article/details/9406019
while(is_list(list)) {
// 通过erlang中传过来的list指针值转换为系统指针
cons = list_val(list);
// 为了观察变量值,我增加了下面这一句
erts_fprintf(stderr,
"element value:%X, new list pointer:%X\n",
cons[0], cons[1]);
// 下面两行是理解list构造的关键,从中我们可以得知:
// cons[0]存储的是list元素值,
// cons[1]存储的是指向下一个list元素,
// 这就表明,list是由单向链表实现的,
// 每一个元素都占用了8个字节的空间,
// 前四个字节存元素值,后四个字节存list的下一个元素的指针值
*hp++ = cons[0];
list = cons[1];
}
BIF_RET(res);
}
现在来启动erl,运行一下上面的函数:
Eshell V5.10.2 (abort with ^G)
1> list_to_tuple([1, 2, 3, "abc", "def"]).
list:25841F5 hp:0x02584248 res:258424A arityval:140
element value:1F, new list pointer:25841B9
element value:2F, new list pointer:25841C1
element value:3F, new list pointer:25841C9
element value:2584949, new list pointer:25841D1
element value:25848B5, new list pointer:FFFFFFFB
{1,2,3,"abc","def"}
我们从输出的第一行中,可以看到
res = make_tuple(hp);
这一句代码的运行结果:
hp:0x02584248 -> res:258424A
即:0x02584248 + 0x2 = 0x258424A
其中0x2就是TAG_PRIMARY_BOXED的值,目的是把指针封装成boxed类型,
但这里有个问题,为什么没有左移两位再相加?
按正常的封装方法应该是:
hp << _TAG_PRIMARY_SIZE | TAG_PRIMARY_BOXED
没有左移两位,是因为指针都是经常字节对齐处理的,
由HAlloc(BIF_P, len+1)返回的指针,最后两位一定会是00B,
所有没有左移的必要,刚好可以用来存放TAG_PRIMARY_BOXED的值。
在输出的结果中可以看到list的最后一个指针值是FFFFFFFB,
这就是《 Erlang数据类型的内部实现》一文中提到的NIL数据类型,
它表示一个list的结尾,或者是一个空list。
我们接着看看tuple_to_list/1函数的内部实现,从中可以看到如保构建一个list。
tuple_to_list/1的内部实现
#define CONS(hp, car, cdr) \
(CAR(hp)=(car), CDR(hp)=(cdr), make_list(hp))
#define CAR(x) ((x)[0])
#define CDR(x) ((x)[1])
BIF_RETTYPE tuple_to_list_1(BIF_ALIST_1)
{
Uint n, m;
Eterm *tupleptr;
// 初始化一个空list
// 即 list = 0xFFFFFFFB
Eterm list = NIL;
Eterm* hp;
if (is_not_tuple(BIF_ARG_1)) {
BIF_ERROR(BIF_P, BADARG);
}
tupleptr = tuple_val(BIF_ARG_1);
// 从tuple的头部取参量值,即为tuple的长度。
n = arityval(*tupleptr);
// 为将要创建的list分配内存空间,
// 从这我们可以知道,list占用的空间接近tuple的两倍,
// 确切的说,list_size = tuple_size * 2 - 4
// 因为tuple有一个4字节的arityval(header),
// 而list是没有header的。
hp = HAlloc(BIF_P, 2 * n);
tupleptr++;
while(n--) {
// n为tuple的长度,n递减,也就是说,
// 从tuple的尾部开始依次取出元素值放入list的头部,
// 然后更新list的头部指针,
// 也就是说,构建list时,list的指针总是保持指向头部,
// 但却是在内存的末端追加元素。
// 因为list的这种实现方式,增删list元素时,
// 最好从头部增删。
list = CONS(hp, tupleptr[n], list);
hp += 2;
}
BIF_RET(list);
}
list与tuple底层初探小结
为什么Erlang的length/1函数比较耗时?
因为list的指针总指向头部的单向链表的实现方式,导致使用length/1函数求list的长度值时,必须要遍历整个list才能求出长度值。
erlang编程时list与tuple之间的选择和使用
tuple可以满足的场景里尽量使用tuple,list会占用更多的内存空间。
list常用于遍历和解析,而tuple却不能,但tuple很适合用于各种匹配。
给list增删元素时,尽量用[H | T]的形式来操作。