1. 程式人生 > >Erlang中list和tuple的構建及轉換的內部實現

Erlang中list和tuple的構建及轉換的內部實現

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