Erlang list的++操作和append函數的底層實現

當提到Erlang中list的++操作符時,我們常會想到它的性能問題。
有些人知道++操作比較耗時,就改用函數append來代替。
到底++操作和append函數之間有什麼區別?


我們來查看一下它們在Erlang源碼及C源碼中的實現。


在$ERL_TOP/lib/stdlib/src/lists.erl可以找到如下代碼:


%% append(X, Y) appends lists X and Y


-spec append(List1, List2) -> List3 when
      List1 :: [T],
      List2 :: [T],
      List3 :: [T],
      T :: term().
 
append(L1, L2) -> L1 ++ L2. %% 使用了++操作


%% append(L) appends the list of lists L


-spec append(ListOfLists) -> List1 when
      ListOfLists :: [List],
      List :: [T],
      List1 :: [T],
      T :: term().


append([E]) -> E;
append([H|T]) -> H ++ append(T); %% 使用了++操作
append([]) -> [].


在$ERL_TOP/erts/emulator/beam/erl_bif_lists.c中可以看到:


/*
 * erlang:'++'/2
 */


Eterm
ebif_plusplus_2(BIF_ALIST_2)
{
    return append(BIF_P, BIF_ARG_1, BIF_ARG_2);
}


BIF_RETTYPE append_2(BIF_ALIST_2)
{
    return append(BIF_P, BIF_ARG_1, BIF_ARG_2);
}


結合上面兩個文件中的源碼,我們可以得出這樣的結論:

++操作,以及函數lists:append/1和lists:append/2,
在Erlang底層都是調用了C函數append(Process* p, Eterm A, Eterm B)來實現。



爲什麼Erlang的++操作耗性能?



要想揭祕++操作是如何耗性能的,我們只有徹底弄清楚它們的底層實現。


一起來看看$ERL_TOP/erts/emulator/beam/erl_bif_lists.c文件中append函數的C實現:


#define CONS(hp, car, cdr) \
        (CAR(hp)=(car), CDR(hp)=(cdr), make_list(hp))


#define CAR(x)  ((x)[0])
#define CDR(x)  ((x)[1])


static BIF_RETTYPE append(Process* p, Eterm A, Eterm B)
{
    Eterm list;
    Eterm copy;
    Eterm last;
    size_t need;
    Eterm* hp;
    int i;


    if ((i = list_length(A)) < 0) {
        BIF_ERROR(p, BADARG);
    }
    if (i == 0) {
        // A的長底爲0,直接返回B
        BIF_RET(B);
    } else if (is_nil(B)) {
        // B是一個空列表,直接返回A
        BIF_RET(A);
    }


    need = 2*i;
    // 爲A的拷貝分配內存空間
    hp = HAlloc(p, need);
    list = A;
    // 首先拷貝A的第一個元素,並初始化copy和last變量
    // 爲什麼要兩個變量指向同一值?
    // 因爲copy指向副本頭部,以下不再對copy的變量值做任何修改,最後用來做函數的返回值
    // last保持指向A副本的最後一個元素,最後要保存B的頭指針值
    copy = last = CONS(hp, CAR(list_val(list)), make_list(hp+2));
    // 更新list變量值,讓它指向A的第2個元素
    list = CDR(list_val(list));
    hp += 2;
    // 注意,上面已經拷貝了A的第一個元素,所以要先i--
    i--;
    // 從A的第2個元素開始,逐個拷貝
    while(i--) {
        // 取出list指向的元素,8字節,
        // CAR(listp)爲指向當前元素值的指針值
        // CDR(listp)爲指向下一個元素的指針值
        Eterm* listp = list_val(list);
        // 注意make_list(hp+2),直至拷貝完成,這個列表的next指針都是指向下一個內存空間,
        // last總是指向最新的元素,因爲是從A的頭部開始拷貝的,
        // 所以last最終也就是指向A副本的最後一個元素
        last = CONS(hp, CAR(listp), make_list(hp+2));
        // 讓list移動到下一個元素
        list = CDR(listp);
        hp += 2;
    }
    // 上面的拷貝之後,副本尾部沒有NIL值,在這裏正好把這個位置的值更新爲B的值,
    // 至此,兩個list的合併完成
    CDR(list_val(last)) = B;
    // 返回副本頭部的指針值
    BIF_RET(copy);
}


以上分析中,我們知道了++操作的具體實現,但我還是有疑問。

爲什麼Erlang中++操作時要拷貝左邊的list?


爲什麼要對A進行拷貝?

如果通過遍歷找到A尾部的NIL值,並把它更新爲B的指針值不就OK了?

這是因爲Erlang的原則是變量一旦賦值就不可再變,如果直接對A進行了修改操作,將會有什麼結果?

我認爲這也是一個在寫NIF時需要注意的問題。


Erlang ++操作符使用小結

  1. 儘量把長度較小的list放在左邊。
  2. 如果左邊的list長度很小,並不會造成很大的性能影響,這種情況下可以放心使用++操作符。
  3. lists:append/2函數最終也是調用++操作來完成,還不如直接使用++明瞭。
  4. 如果需要合併的list個數是未知的,可以使用函數lists:append/1來完成。


Erlang數據類型的內部實現

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





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章