Erlang中list和tuple的構建及轉換的內部實現
阿新 • • 發佈:2018-12-30
為探索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,
這就是《
它表示一個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才能求出長度值。