Erlang List結構和效能分析

2021-08-28 21:16:57 字數 2795 閱讀 9515

erlang裡通過尾遞迴方式對列表中元素依次進行操作時,程式設計師們採用的方法總是先在尾遞迴中將處理後的元素加在已處理列表的頭部,最後通過lists:reverse(list)來恢復原來次序。為什麼不直接以自然順序將表頭元素加到已處理列表的尾部呢?這裡面都是有故事的。

先看兩個函式:

123

4567

891011

1213

1415

1617

-module(list_time).

-export([add_list_head/1, add_list_tail/1]).

add_list_head(n) ->

add_list_head(n, ).

add_list_head(0, l) ->

lists:reverse(l),

ok;add_list_head(n, l) ->

add_list_head(n-1, [65 | l]).

add_list_tail(n) ->

add_list_tail(n, ).

add_list_tail(0, l) ->

ok;add_list_tail(n, l) ->

add_list_tail(n-1, l ++ [65]).

add_list_head/1和add_list_tail/1都用於依次向列表中加入乙個元素,元素個數由引數n指定。add_list_head/1中,新加入的元素放在當前列表的表頭,最後呼叫lists:reverse(l)恢復順序;add_list_tail/1中,新加入的元素直接放在當前列表的表尾。add_list_tail/1的實現方法更符合我們的邏輯,但效率上卻是add_list_head/1遠遠佔優。有如下基準測試資料為證:

123

4567

891011

1213

1415

16

1> c (list_time).

2> timer:tc(list_time, add_list_head, [10000]).

3> timer:tc(list_time, add_list_head, [100000]).

4> timer:tc(list_time, add_list_head, [1000000]).

5> timer:tc(list_time, add_list_head, [10000000]).

6>

6> timer:tc(list_time, add_list_tail, [10000]).

7> timer:tc(list_time, add_list_tail, [100000]).

8> timer:tc(list_time, add_list_tail, [1000000]). %半小時仍未返回 =。=

從測試資料看,add_list_head/1在依次新增10k, 100k, 1m, 10m個元素的時間分別為0.56ms, 8.87ms, 98.27ms, 1745.46ms。而add_list_tail/1新增10k, 100k個元素的時間分別為561.63ms和59663.92ms,當元素個數為1m時,執行半個小時仍未返回。造成效能差距如此之大的原因得從列表在erlang裡的實現原理中找。

在erlang裡,空列表用表示,所有非空列表都可以用[element | remain]的方式表示,比如,[a]可以表示成[a | ],[a, b, c]等價於[a | [b | [c | ]]]。但列表不是通過陣列實現的,實際上,列表的底層資料結構是乙個單鏈表(singly-linked list)。如下列表定義:

1
foo = [a, b].

其對應的儲存結構為:

1

23

foo

↓ a → b → null

其中,foo可以看做是單鏈表a→b的表頭指標。

現在分析向表頭新增元素生成列表的情況,向foo表頭新增元素c:

1
bar = [c | foo].

它的儲存結構為:

1

23

bar foo

↓ ↓

c → a → b → null

向表頭新增元素c只需把該節點的next指標指向foo列表的表頭即可,時間複雜度是o(1)。如果再向foo的表頭新增其他元素,如:

1
baz = [d, e | foo].

那麼,baz的儲存結構為:

123

4567

bar foo

↓ ↓

c → a → b → null

↑d → e

↑baz

這個例子中, bar, baz直接重用了foo的元素(儲存空間),因為foo是不可變的,所以這樣的處理方式沒有***。由此可以看出,通過向表頭新增元素生成列表的方式的時間複雜度是o(n)。

1

23

list1 = [a, b],

list2 = [c, d],

list3 = list1 ++ list2.

構造列表list3的過程中,list1的資料被複製了乙份,依次加到列表list2的表頭,每次++操作,都會產生乙個新的臨時列表!從底層的指令看,每次表尾追加元素總是相當於先遍歷一遍list1,所以向表尾追加元素的時間複雜度是o(n2)!

通過表頭和表尾來操作列表的時間複雜度分別是o(n)和o(n2),因此,提倡以add_list_head/1的方式來操作列表。

程式結構和效能優化

最近在寫一套關於blog遷移的程式,由於涉及到頁面分析就寫了乙個笨笨的迴圈檢測程式 有段 大致如下 public string t1 string str 其中countlength是個計算方法,計算str中 出現次數 這樣的一段 你認為會有問題嗎?至少我最初是這麼寫的也沒有發現有什麼問題。但是,後...

併發資料結構 1 1 1 效能

乙個執行在p個處理上的應用程式的加速度是它在單個處理器上的執行時間和在p個處理器的執行時間的比值。這是一種評價應用程式對於機器資源利用程度的衡量。理想情況下,我們想要的結果是線性加速度 當我們使用p個處理器的時候,我們希望可以獲得p的加速度 譯者注 例如乙個應用程式在單處理器的執行時間是10秒,那麼...

併發資料結構 1 1 1 效能

乙個執行在p個處理上的應用程式的加速度是它在單個處理器上的執行時間和在p個處理器的執行時間的比值。這是一種評價應用程式對於機器資源利用程度的衡量。理想情況下,我們想要的結果是線性加速度 當我們使用p個處理器的時候,我們希望可以獲得p的加速度 譯者注 例如乙個應用程式在單處理器的執行時間是10秒,那麼...