【譯】構造和匹配二進制(Efficiency Guide)

能夠經過如下方式有效地構建二進制:ide

my_list_to_binary(List) ->函數

    my_list_to_binary(List, <<>>).優化

my_list_to_binary([H|T], Acc) ->ui

    my_list_to_binary(T, <<Acc/binary,H>>);spa

my_list_to_binary([], Acc) ->指針

    Acc.對象

二進制能夠像這樣有效地匹配:進程

my_binary_to_list(<<H,T/binary>>) ->ci

    [H|my_binary_to_list(T)];編譯器

my_binary_to_list(<<>>) -> [].

4.1如何實現二進制

在內部,二進制和位串以相同的方式實現。在本節中,它們被稱爲二進制,由於這就是它們在模擬器源代碼中的名稱。

內部有四種類型的二進制對象:

  • 兩個是二進制數據的容器,它們被稱爲:
    • Refc Binaries引用計數二進制的縮寫)
    • 堆二進制
  • 兩個僅僅是對二進制一部分的引用,它們被稱爲:
    • 子二進制
    • 匹配上下文

Refc二進制

Refc二進制包含兩個部分:

  • 存儲在進程堆上的對象,稱爲ProcBin
  • 二進制對象自己,存儲在全部進程堆的外部

二進制對象可由任意數量的進程中的任意數量的ProcBins引用。該對象包含一個引用計數器,以跟蹤引用的數量,以便在最後一個引用消失時能夠將其刪除。

進程中的全部ProcBin對象都是連接列表的一部分,所以,當ProcBin消失時,垃圾收集器能夠跟蹤它們並減小二進制中的引用計數器。

堆二進制

堆二進制是小型二進制,最多64個字節,並直接存儲在進程堆中。當進程被垃圾回收而且做爲消息發送時,它們被複制。它們不須要垃圾收集器進行任何特殊處理。

子二進制

引用對象子二進制匹配上下文能夠引用refc二進制或堆二進制的一部分。

子二進制經過建立split_binary/2,而且當二進制以二進制模式匹配的。子二進制是對另外一個二進制(refc或堆二進制,而不是對另外一個子二進制)的一部分的引用。所以,匹配二進制相對便宜,由於從不復制實際的二進制數據。

匹配上下文

匹配上下文相似於子二進制,但對於二進制匹配被優化。例如,它包含一個指向二進制數據的直接指針。對於從二進制中匹配的每一個字段,匹配上下文中的位置會增長。

編譯器試圖避免生成用於建立子二進制的代碼,而只是在不久以後建立一個新的匹配上下文並丟棄該子二進制。保留匹配上下文,而不是建立子二進制。

若是編譯器知道不會共享匹配上下文,則只能進行此優化。若是將其共享,則Erlang的功能屬性(也稱爲參照透明性)將中斷。

4.2構造二進制

運行時系統特別優化了附加到二進制或位串的操做:

<<Binary/binary, ...>>

<<Binary/bitstring, ...>>

當運行時系統處理優化(而不是編譯器)時,在極少數狀況下優化不起做用。

爲了解釋它是如何工做的,讓咱們逐行檢查如下代碼:

Bin0 = <<0>>,                    %% 1

Bin1 = <<Bin0/binary,1,2,3>>,    %% 2

Bin2 = <<Bin1/binary,4,5,6>>,    %% 3

Bin3 = <<Bin2/binary,7,8,9>>,    %% 4

Bin4 = <<Bin1/binary,17>>,       %% 5 !!!

{Bin4,Bin3}                      %% 6

  • 1行(標有%% 1註釋)將堆二進制分配給Bin0變量。
  • 2行是追加操做。因爲Bin0還沒有參與追加操做,新的REFC二進制被建立和內容Bin0被複制到它。refc二進制的ProcBin部分的大小設置爲存儲在二進制中的數據的大小,而二進制對象分配了額外的空間。二進制對象的大小是Bin1256的大小的兩倍,以較大者爲準。在這種狀況下爲256。
  • 3行更有趣。Bin1已經在附加操做中使用,而且它具備252個字節在末端未使用的存儲空間,因此3個新字節被存儲在那裏。
  • 4行。此處一樣適用。剩下249個字節,所以存儲另外3個字節沒有問題。
  • 5行。在這裏,發生了一些有趣的事情。請注意,結果不追加到之前的結果Bin3,但Bin1。預期將爲Bin4賦值<<0,1,2,3,17>>。還能夠預期Bin3將保留其值(<<0,1,2,3,4,5,6,7,8,9>>)。顯然,運行時系統沒法將字節17寫入二進制,由於這會將Bin3的值更改<<0,1,2,3,4,17,6,7,8,9>>

運行時系統會發現Bin1是上一個追加操做(不是最新的追加操做)的結果,所以它將Bin1的內容複製到新的二進制中,保留了額外的存儲空間,依此類推。(這裏沒有解釋運行時系統如何知道不容許將其寫入Bin1;好奇的讀者能夠將其做爲練習,經過讀取仿真器源代碼(主要是erl_bits.c)來了解如何完成此操做。)

強制複製的狀況

二進制追加操做的優化要求,有一個單一ProcBin和一個單一的引用ProcBin用於二進制。緣由是能夠在追加操做期間移動(從新分配)二進制對象,而且在這種狀況下,必須更新ProcBin中的指針。若是將有多個ProcBin指向二進制對象,則將不可能找到並更新全部它們。

所以,對二進制的某些操做會對其進行標記,以便未來任何附加操做都將被強制複製二進制。在大多數狀況下,二進制對象將同時縮小以回收分配給增加的額外空間。

當按以下所示追加到二進制時,僅從最新的追加操做返回的二進制將支持進一步的廉價追加操做:

Bin = <<Bin0,...>>

在本節開頭的代碼片斷中,追加到Bin將很便宜,而追加到Bin0將強制建立新的二進制並複製Bin0的內容。

若是將二進制做爲消息發送到進程或端口,則該二進制將縮小,而且任何進一步的追加操做會將二進制數據複製到新的二進制中。例如,在下面的代碼片斷中,Bin1將被複制到第三行:

Bin1 = <<Bin0,...>>,

PortOrPid!Bin1

Bin = <<Bin1,...>>   %% Bin1將被複制

若是將二進制插入到Ets表中,或者使用erlang:port_command/2將其發送到端口,或者將其傳遞給NIF中的enif_inspect_binary也會發生一樣的狀況。

匹配二進制也將致使其縮小,而且下一個追加操做將複製二進制數據:

Bin1 = <<Bin0,...>>,

<< X,Y,Z,T/binary>> = Bin1,

Bin = <<Bin1,...>>   %% Bin1將被複制

緣由是匹配上下文包含指向二進制數據的直接指針。

若是進程僅保留二進制(在「循環數據」中或在進程字典中),則垃圾收集器最終能夠收縮二進制。若是隻保留一個這樣的二進制,它將不會縮小。若是該過程稍後追加到已縮小的二進制中,則將從新分配二進制對象以放置要附加的數據。

4.3匹配二進制

讓咱們回顧上一節開頭的示例:

my_binary_to_list (<< H,T/binary >>) ->

    [H | my_binary_to_list(T)];

my_binary_to_list (<< >>) -> []。

首次調用my_binary_to_list/1時,將建立一個匹配上下文。匹配上下文指向二進制的第一個字節。1個字節被匹配,而且匹配上下文被更新以指向二進制中的第二個字節。

在這一點上,建立一個子二進制是有意義的,可是在此特定示例中,編譯器發現很快將調用一個函數(在本例中爲my_binary_to_list/1自己),該函數將當即建立一個新的匹配上下文並丟棄子二進制。

所以,my_binary_to_list/1會使用match上下文而不是子二進制進行調用。初始化匹配操做的指令在看到已傳遞給匹配上下文而不是二進制時,基本上什麼也不作。

當到達二進制的末尾而且第二個子句匹配時,匹配上下文將被簡單地丟棄(在下一個垃圾回收中將其刪除,由於再也不有對其的引用)。

總而言之,my_binary_to_list/1僅須要建立一個匹配上下文,而無需子二進制。

請注意,遍歷整個二進制後,將放棄my_binary_to_list/1中的match上下文。若是迭代在到達二進制末尾以前中止,會發生什麼狀況?優化是否仍然有效?

after_zero(<<0,T/binary>>) ->

    T;

after_zero(<<_,T/binary>>) ->

    after_zero(T);

after_zero(<<>>) ->

    <<>>.

是的,它會的。編譯器將在第二個子句中刪除子二進制的構建:

...

after_zero(<<_,T/binary>>) ->

    after_zero(T);

...

可是它將生成在第一個子句中構建子二進制的代碼:

after_zero(<<0,T/binary>>) ->

    T;

...

所以,after_zero/1構建一個匹配上下文和一個子二進制(假定傳遞了一個包含零字節的二進制)。

以下代碼也將獲得優化:

all_but_zeroes_to_list(Buffer, Acc, 0) ->

    {lists:reverse(Acc),Buffer};

all_but_zeroes_to_list(<<0,T/binary>>, Acc, Remaining) ->

    all_but_zeroes_to_list(T, Acc, Remaining-1);

all_but_zeroes_to_list(<<Byte,T/binary>>, Acc, Remaining) ->

    all_but_zeroes_to_list(T, [Byte|Acc], Remaining-1).

編譯器在第二和第三子句中刪除了子二進制的構建,並向第一子句添加了一條指令,該指令將Buffer從匹配上下文轉換爲子二進制(若是Buffer已是二進制,則不執行任何操做)。

可是在更復雜的代碼中,如何知道是否應用了優化呢?

選項bin_opt_info

使用bin_opt_info選項可以使編譯器打印許多有關二進制優化的信息。能夠將其提供給編譯器或erlc

erlc +bin_opt_info Mod.erl

或經過環境變量傳遞:

export ERL_COMPILER_OPTIONS=bin_opt_info

注意,bin_opt_info並非要添加到Makefile的永久選項,由於它生成的全部消息都沒法消除。所以,在大多數狀況下,將選項傳遞給環境是最實用的方法。

警告以下:

./efficiency_guide.erl:60:警告:未優化:該函數返回了二進制

./efficiency_guide.erl:62:警告:已優化:匹配上下文已重用

爲了更清楚地說明警告所指的代碼,例如,如下示例中的警告以註釋的形式插入它們所引用的子句以後,例如:

after_zero (<< 0,T/binary>>) -> %% BINARY CREATED:從函數返回二進制

    T;

after_zero (<< _,T/binary >>) -> %%優化:重用匹配上下文

    after_zero(T);

after_zero (<< >>) ->

    << >>。

第一個子句的警告說,不能延遲子二進制的建立,由於它將被返回。第二個子句的警告說將不會建立子二進制。

未使用的變量

編譯器會肯定變量是否未使用。爲如下每一個功能生成相同的代碼:

count1(<<_,T/binary>>, Count) -> count1(T, Count+1);

count1(<<>>, Count) -> Count.

count2(<<H,T/binary>>, Count) -> count2(T, Count+1);

count2(<<>>, Count) -> Count.

count3(<<_H,T/binary>>, Count) -> count3(T, Count+1);

count3(<<>>, Count) -> Count.

在每次迭代中,二進制中的前8位將被跳過,不匹配。

4.4歷史記錄

R12B中的二進制處理獲得了顯着改善。因爲在R11B中有效的代碼在R12B中可能無效,反之亦然,所以本《效率指南》的較早版本包含一些有關R11B中二進制處理的信息。

相關文章
相關標籤/搜索