可以通過以下方式有效地構建二進制:
my_list_to_binary(List) ->
my_list_to_binary(List, <<>>).
my_list_to_binary([H|T], Acc) ->
my_list_to_binary(T, <<Acc/binary,H>>);
my_list_to_binary([], Acc) ->
Acc.
二進制可以像這樣有效地匹配:
my_binary_to_list(<<H,T/binary>>) ->
[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部分的大小設置為存儲在二進制中的數據的大小,而二進制對象分配了額外的空間。二進制對象的大小是Bin1或256的大小的兩倍,以較大者為準。在這種情況下為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中二進制處理的信息。