實例分析Erlang二進制(Binary)的匹配優化

從《實例分析Erlang二進制(Binary)匹配的內部機制》一文中瞭解到二進制匹配機制,現在該是付諸行動、動手優化Erlang程序匹配操作的時候了。

先看來自《Efficiency Guide User's Guide》的兩個Erlang函數實例,它們的功能都是判斷一個list和一個binary的內容是否相等。

test.erl
-module(test).
-compile(export_all).


non_opt_eq([H|T1], <<H,T2/binary>>) ->
    non_opt_eq(T1, T2);
non_opt_eq([_|_], <<_,_/binary>>) ->
    false;
non_opt_eq([], <<>>) ->
    true.


opt_eq(<<H,T1/binary>>, [H|T2]) ->   %% 分支1
    opt_eq(T1, T2);
opt_eq(<<_,_/binary>>, [_|_]) ->     %% 分支2
    false;
opt_eq(<<>>, []) ->                  %% 分支3
    true.
對比上面兩個函數,依我個人習慣,在Erlang編程時會選擇第一種寫法。
這兩個函數功能上來說是一樣的,它們的內部實現會有什麼不同呢?

Erlang語言在二進制匹配方面做了一些特別的優化工作,但並不是任何情景下都能利用這一特性。
至於我們編寫的Erlang代碼中有沒有用到這些優化特性,經常是不明確的。
爲了解決這一問題,Erlang爲我們提供了打印二進制優化信息的方法。

方法一
erlc +bin_opt_info xxx.erl
編譯時加上+bin_opt_info選項。

方法二
export ERL_COMPILER_OPTIONS=bin_opt_info
導出一個系統的環境變量,然後按正常編譯。

加上優化選項編譯本例: erlc +bin_opt_info test.erl
打印了以下信息:
test.erl:4: Warning: INFO: matching anything else but a plain variable to the left of binary pattern will prevent delayed sub binary optimization; SUGGEST changing argument order
test.erl:4: Warning: NOT OPTIMIZED: called function non_opt_eq/2 does not begin with a suitable binary matching instruction
test.erl:11: Warning: OPTIMIZED: creation of sub binary delayed
初看這些信息,還不太明白它說的是什麼。爲了弄明白,我們來分析一下它的彙編指令。

(關於彙編指令的介紹可參見《實例分析Erlang的彙編指令》一文)


erlc +"'S'" test.erl
%% test.S

{function, non_opt_eq, 2, 2}.
  {label,1}.
    {line,[{location,"test.erl",4}]}.
    {func_info,{atom,test},{atom,non_opt_eq},2}.
  {label,2}.
    %% 如果R0爲空列表,則返回true
    {test,is_nonempty_list,{f,4},[{x,0}]}.
    %% 從list中取頭元素送R2
    {get_list,{x,0},{x,2},{x,3}}.
    %% 初始化match context二進制
    {test,bs_start_match2,{f,1},4,[{x,1},0],{x,4}}.
    %% 從二進制流中取出一個整型(1字節)
    {test,bs_get_integer2,
          {f,1},
          5,
          [{x,4},
           {integer,8},
           1,
           {field_flags,[{anno,[4,{file,"test.erl"}]},unsigned,big]}],
          {x,5}}.
    %% 取出剩餘的二進制,並創建新的sub binary,送R6
    {test,bs_get_binary2,
          {f,1},
          6,
          [{x,4},
           {atom,all},
           8,
           {field_flags,[{anno,[4,{file,"test.erl"}]},unsigned,big]}],
          {x,6}}.
    %% '%'是註釋,打印優化信息
    {'%',{no_bin_opt,{{label,2},no_suitable_bs_start_match},
                     [4,{file,"test.erl"}]}}.
    {test,is_eq_exact,{f,3},[{x,5},{x,2}]}.
    %% R6(新創建的sub binary)送R1,爲下一次迭代準備參數
    {move,{x,6},{x,1}}.
    {move,{x,3},{x,0}}.
    {call_only,2,{f,2}}.
  {label,3}.
    {move,{atom,false},{x,0}}.
    return.
  {label,4}.
    {test,is_nil,{f,1},[{x,0}]}.
    {test,is_eq_exact,{f,1},[{x,1},{literal,<<>>}]}.
    {move,{atom,true},{x,0}}.
    return.




{function, opt_eq, 2, 6}.
  {label,5}.
    {line,[{location,"test.erl",11}]}.
    {func_info,{atom,test},{atom,opt_eq},2}.
  {label,6}.
    %% 初始化match context二進制,結果送R0
    {test,bs_start_match2,{f,5},2,[{x,0},0],{x,0}}.
    %% 從二進制流中取出一個整型(1字節)
    {test,bs_get_integer2,
          {f,8},
          2,
          [{x,0},
           {integer,8},
           1,
           {field_flags,[{anno,[11,{file,"test.erl"}]},unsigned,big]}],
          {x,2}}.
    {'%',{bin_opt,[11,{file,"test.erl"}]}}.
    {test,bs_test_unit,{f,9},[{x,0},8]}.
    %% R0送R3
    {move,{x,0},{x,3}}.
    {test,is_nonempty_list,{f,9},[{x,1}]}.
    {get_list,{x,1},{x,4},{x,5}}.
    {test,is_eq_exact,{f,7},[{x,4},{x,2}]}.
    {move,{x,5},{x,1}}.
    %% R3送回R0
    {move,{x,3},{x,0}}.
    {call_only,2,{f,6}}.
  {label,7}.
    {move,{atom,false},{x,0}}.
    return.
  {label,8}.
    {test,bs_test_tail2,{f,9},[{x,0},0]}.
    {test,is_nil,{f,9},[{x,1}]}.
    {move,{atom,true},{x,0}}.
    return.
  {label,9}.
    {bs_context_to_binary,{x,0}}.
    {jump,{f,5}}.
上面的指令中可以明顯看出 opt_eq函數的分支2沒有創建sub binary,而是直接把match context類型的二進制作爲下一次迭代的參數。
bs_start_match2指令也做了優化,如果參數是match context類型就不會再次創建match context二進制。

non_opt_eq函數爲什麼就沒有進行這些優化?
要解答這個問題,先要了解二進制的創建機制,關於這方面的內容前面的文章裏已經介紹過,這裏就不再贅述。

在本例中,sub binary是否要被創建,取決於編譯器是否能確定match context二進制不被共享。

像opt_eq函數,binary匹配作爲第一個參數,可以確定函數的每一個分支都首先會進行匹配操作,只要binary不爲空,匹配前都要先創建match context(如果它不是match context),這些情況編譯器都是可以確定的,所以沒有必要每次迭代都創建sub binary,故可進行優化。

而 non_opt_eq函數,在某個分支裏binary是否要進行匹配操作要依賴於第一個參數,也可能在某個分支裏被共享,如:
non_opt_eq([H|T1], <<H,T2/binary>>) ->
    non_opt_eq(T1, T2);
non_opt_eq([_|_], T) ->
    mod:fun(T).

目前所有類似以下形式的代碼都不會被優化:
match_body([0|_], <<H,_/binary>>) ->
    done;
如果編譯時加了優化選項,會提示你調整參數的順序(SUGGEST changing argument order)。

就non_opt_eq這個函數而言,理論上編譯器是可以對它進行優化的,不過目前沒有實現,或許將來的版會實現這一點。

小結

如果函數參數中有匹配操作的參數,根據實際的情況儘量將它放在首位。

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