Erlang中list和tuple的构建及转换的内部实现

Erlang数据类型的内部实现


为探索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]的形式来操作。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值