1. 程式人生 > >[Erlang 0127] Term sharing in Erlang/OTP 上篇

[Erlang 0127] Term sharing in Erlang/OTP 上篇

之前,在 [Erlang 0126] 我們讀過的Erlang論文 提到過下面這篇論文: 

On Preserving Term Sharing in the Erlang Virtual Machine
地址: http://user.it.uu.se/~kostis/Papers/erlang12_sharing.pdf 
摘要:In this paper we describe our experiences and argue through examples why flattening terms during copying is not a good idea for
a language like Erlang. More importantly, we propose a sharing preserving copying mechanism for Erlang/OTP and describe a pub-
licly available complete implementation of this mechanism. 

   Term Sharing  資料項共享這個東西不新鮮,"Efficiency Guide User's Guide"的4.2章節"Constructing binaries"[

連結 ] 和 8.2 章節的"Loss of sharing" [連結]就提到過(再次吐槽一下Erlang文件組織,一個主題往往分散在多個文件裡面,需要耐心).不僅僅是Binary資料型別,Term Sharing 是一個通用的問題.在Guide中提到了強制拷貝的場景:傳送到別的程序或者插入到ETS. 下面是文件摘抄的:

   Loss of sharing
  
  Shared sub-terms are not preserved when a term is sent to another process, passed as the initial process arguments in the spawn call, or stored in an ETS table. That is an optimization. Most applications do not send messages with shared sub-terms.

   資料拷貝過程,Erlang會進行兩次遍歷.第一次遍歷,會計算flat size(erts/emulator/beam/copy.c中的size_object 方法)然後為此分配對應的記憶體,第二次遍歷完成實際的拷貝 (function copy_structin erts/emulator/beam/copy.c).

首先,我們寫一個簡單的程式碼演示一下後面要頻繁用到的erts_debug:size/1和erts_debug:flat_size/1

s3(L)->
    L2=[L,L,L,L],
    {{erts_debug:size(L),erts_debug:flat_size(L)},
      {erts_debug:size(L2),erts_debug:flat_size(L2)}}
.

9> d:s3([1,2,3,4,5,6]).
{{12,12},{20,56}}

  下面在shell這段程式碼,分別演示了spawn,訊息傳送,以及插入ETS.

Eshell V6.0  (abort with ^G)
1> L=[1,2,3,4,5,6,7,8,9,10].
[1,2,3,4,5,6,7,8,9,10]
2>  L2=[L,L,L,L,L,L].
[[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10]]
3>  erts_debug:size(L2).
32
4>  erts_debug:flat_size(L2).
132
5>  spawn(fun () ->receive Data ->  io:format("~p",[erts_debug:size(Data)]) end end).
<0.39.0>
6> v(5) ! L2.
132[[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10]]
7>  erts_debug:size(L2).
32
8> ets:new(test,[named_table]).
test
9> ets:insert(test,{1,L2}).
true
10>  ets:lookup(test ,1).
[{1,
  [[1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10]]}]
11> [{1,Data}]=v(10).
[{1,
  [[1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10]]}]
12> Data.
[[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10]]
13>  erts_debug:size(Data).
132
14> spawn(d,test,[L2]).
132<0.54.0>

 test(Data)->
   io:format("~p",[erts_debug:size(Data)]).

   

  除了上面的情況,還有一些潛在的情況也會導致資料展開,比如上面提到的論文裡面設計的例子:

show_printing_may_be_bad() ->
  F = fun (N) ->
  T = now(),
  L = mklist(N),
  S = erts_debug:size(L),
  io:format("mklist(~w), size ~w, ", [N, S]),
  io:format("is ~P, ", [L, 2]), %%% BAD !!!
  D = timer:now_diff(now(), T),
  io:format("in ~.3f sec.~n", [D/1000000])
   end,
   lists:foreach(F, [10, 20, 22, 24, 26, 28, 30]).

mklist(0) -> 0;
mklist(M) -> X = mklist(M-1), [X, X].

  io:format("is ~P, ", [L, 2]), %%% BAD !!!這行程式碼刪掉前後分別執行程式碼,在過機器上得到的結果如下:

Eshell V6.0  (abort with ^G)
1>  d:show_printing_may_be_bad().
mklist(10), size 40, in 0.001 sec.
mklist(20), size 80, in 0.000 sec.
mklist(22), size 88, in 0.000 sec.
mklist(24), size 96, in 0.000 sec.
mklist(26), size 104, in 0.000 sec.
mklist(28), size 112, in 0.000 sec.
mklist(30), size 120, in 0.000 sec.
ok


Eshell V6.0  (abort with ^G)
1>  d:show_printing_may_be_bad().
mklist(10), size 40, is [[...]|...], in 0.001 sec.
mklist(20), size 80, is [[...]|...], in 0.110 sec.
mklist(22), size 88, is [[...]|...], in 0.421 sec.
mklist(24), size 96, is [[...]|...], in 43.105 sec.
mklist(26), size 104, 
Crash dump was written to: erl_crash.dump
eheap_alloc: Cannot allocate 3280272216 bytes of memory (of type "heap").
rlwrap: warning: erl killed by SIGABRT.
rlwrap has not crashed, but for transparency,
it will now kill itself (without dumping core)with the same signal

   很明顯看到有這行程式碼的版本不僅執行時間長,而且需要大量記憶體.

   為什麼會出現這種情況呢?就是上面提到的"Loss of sharing",為什麼會觸發資料展開(或者說資料平鋪化)呢?之前我們曾經聊過關於io:format的事情( [Erlang 0041] 詳解io:format [連結]),在Erlang/OTP中I/O是通過向I/O Server發起I/O請求實現的.io:format呼叫實際上是向I/O 傳送了一個io request訊息,剩下的就由IO Server處理了.雖然上面的L被簡略的輸出為" [[...]|...]"但是在訊息的傳遞過程中已經觸發了資料平鋪然後拷貝;

   這種問題實際上是非常隱蔽的,所以通過巨集選項在release生成的時候清理掉所有io:format輸出是非常有必要的;