Erlangのプロセスは、オペレーティングシステムのスレッドやプロセスと比べて軽量です。
Erlangのプロセスを新規の生成する場合には、HiPEサポートなしで、SMPエミュレータがない環境では、309ワード(32ビット環境では1ワード4バイト)のメモリを消費します。SMPサポートおよび、HiPEサポートを使用すると、それぞれサイズが大きくなります)。このサイズは以下のようにして核にすることができます:
Erlang (BEAM) emulator version 5.6 [async-threads:0] [kernel-poll:false]
Eshell V5.6 (abort with ^G)
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<...>
2> {_,Bytes} = process_info(spawn(Fun), memory).
{memory,1232}
3> Bytes div erlang:system_info(wordsize).
309
このプロセスのサイズには233ワードのヒープ領域(スタックも含みます)が含まれます。ガーベジコレクタは必要に応じてヒープの領域を増加させます。
プロセスのメイン(一番外側)ループは 末尾再帰にしなければなりません 。もし末尾再帰にしなかった場合には、ループごとにスタックを消費して、プロセスが異常終了することになります。
非推奨:
loop() ->
receive
{sys, Msg} ->
handle_sys_msg(Msg),
loop();
{From, Msg} ->
Reply = handle_msg(Msg),
From ! Reply,
loop()
end,
io:format("Message is processed~n", []).
io:format/2の呼び出しは行われません。代わりにリターンアドレスがloop/0の一回の呼び出しごとにスタックに積まれていきます。正しい末尾再帰バージョンは以下のようになります:
.. DO
推奨:
loop() ->
receive
{sys, Msg} ->
handle_sys_msg(Msg),
loop();
{From, Msg} ->
Reply = handle_msg(Msg),
From ! Reply,
loop()
end.
デフォルトの初期ヒープサイズが233ワードしかない理由としては、Erlangのシステムが何十万、何百万というプロセス数をサポートをするために、極めて保守的に設定されているからです。ガーベジコレクタは必要に応じて、ヒープのサイズを拡大したり、縮小したりします。
比較的プロセス数が少ないシステムでは、elrの+hオプションを使用するか、CPU1プロセスに対して、プロセス数が1のシステムではspawn_opt/4のmin_heap_sizeオプションを使用して最低ヒープサイズを増やすことで、パフォーマンスが上がる可能性があります。
メリットとしては2要素あります。まず最初に、ガーベジコレクタはヒープサイズを拡大しますが、少しずつ増加させていきます。この場合、少しずつメモリを確保するよりも、大きなサイズのヒープをプロセス生成時に直接確保した方が効率的である、という点です。2番目は、ガーベジコレクタがヒープサイズを縮小する場合になります。これはヒープが必要量よりもはるかに多い量確保されている場合に縮小が行われますが、最小のヒープサイズを設定することで、これを防ぐことができます。
Warning
エミュレータはおそらくより多くのメモリを消費します。というのは、ガーベジコレクタが実行される回数が少なく、より大きなバイナリデータが長期間保持されることになるからです。
プロセス数が多いシステムの場合には、新しいプロセスのヒープサイズが小さいほど、処理するタスクの生成にかかる時間が少なくなります。もしプロセスが完了した場合には、計算結果は他のプロセスに送られ、終了します。計算を行うのに必要最低限のプロセスサイズが設定されている場合にはガーベジコレクションはまったく実行されない可能性があります。 最適化を行う場合には適切に測定せずに行おうとしてはいけません。
Erlangプロセス間のメッセージに含まれる全てのデータは、同じErlangノード上のrefcバイナリを覗いて、コピーされます。
メッセージが他のErlangノードに送信される場合には、まず最初に、Erlang外部フォーマットと呼ばれるものにエンコードされて、TCP/IPソケットを通じて送信されます。受信側のErlangノードは、まずはメッセージをデコードし、正しいプロセスに分配します。
定数Erlang項(リテラルとも呼ばれる)は定数プールというところに保存されます。ロードされたモジュールごとに、それぞれプールが存在します。以下のような関数があったとします。
推奨(R12B以降):
days_in_month(M) ->
element(M, {31,28,31,30,31,30,31,31,30,31,30,31}).
この関数を実行しても、ガーベジコレクタが実行された次の回に実行された時を除き、毎回タプルが生成されることはありません。このタプルはモジュールの定数プール内に配置されます。
しかし、定数が他のプロセスに送信されたり、ETSテーブルに保存される場合にはコピーされることになります。この理由というのは、ランタイムシステムは定数を含むコードを、適切なタイミングでアンロードできるように、すべての定数の参照を追跡できるようになっていなければならないのですが、他のプロセスなどに行ってしまうと、追跡が難しいため、コピーされます。コードがアンロードされると、その定数はプロセスのヒープにコピーされます。定数のコピーは、将来のリリースで削除される可能性があります。
sub-termの共有は、termが他のプロセスに送信するときにも保護されません。初期のプロセスの引数として生成の呼び出し時に渡されるか、ETSテーブルの中に格納されます。これは最適化です。ほとんどのアプリケーションでは、メッセージの送信時にはsub-termの共有は行いません。
共有sub-termはどのようにしたら作成されるのか、というサンプルを以下に示します:
kilo_byte() ->
kilo_byte(10, [42]).
kilo_byte(0, Acc) ->
Acc;
kilo_byte(N, Acc) ->
kilo_byte(N-1, [Acc|Acc]).
kilo_byte/1 は深いリストを作成します。もし list_to_binary/1 を呼び出すと、このディープリストは1024バイトのバイナリに変換されます:
1> byte_size(list_to_binary(efficiency_guide:kilo_byte())).
1024
erts_debug:size/1 という組み込み関数を使用すると、この深いリストが22ワードのヒープ領域しか使用していないことを確認することができます:
2> erts_debug:size(efficiency_guide:kilo_byte()).
22
erts_debug:flat_size/1 組み込み関数を使用すると、共有が無視されていれば、深いリストのサイズを計算することができます。このサイズは、他のプロセスに送信されたり、ETSテーブルに格納されたりする場合のサイズになります:
3> erts_debug:flat_size(efficiency_guide:kilo_byte()).
4094
もしデータをETSテーブルに格納すると、共有が失われることを確認できます:
4> T = ets:new(tab, []).
17
5> ets:insert(T, {key,efficiency_guide:kilo_byte()}).
true
6> erts_debug:size(element(2, hd(ets:lookup(T, key)))).
4094
7> erts_debug:flat_size(element(2, hd(ets:lookup(T, key)))).
4094
データがETSテーブルに渡されると、 erts_debug:size/1 と erts_debug:flat_size/1 は同じ数値を返すようになります。共有はここで失われたと言うことが分かります。
Erlang/OTPの将来のリリースでは、オプションで、共有を保存する機能を実装しようと考えています。共有の保存に関して、デフォルトの振る舞いをどのようにするかはまだ計画がありませんが、これが導入されると、多くのErlangアプリケーションにとっては、ペナルティがあるでしょう。
R11Bから導入されたSMPエミュレータにより、マルチコアやマルチCPUのコンピュータ上でのErlangがスケジューリングしているスレッドの実行が改善されるでしょう。一般的にはコア数と同数のスレッドでもっとも効果を発揮するでしょう。それぞれのスケジューラスレッドはSMPエミュレータがないErlangのスケジューラと同じようにErlangプロセスをスケジューリングします。
SMPエミュレータを使用してパフォーマンスを古城させるには、ほとんどの箇所において、一つ以上のErlangプロセスが走るようなアプリケーション構造にする必要があります。そうでなければ、Erlangエミュレータは同時に一つのErlangプロセスしか実行することができません。それだけではなく、マルチプロセス用のロックのオーバーヘッドのコストも支払う必要があります。ロックのオーバーヘッドをできるだけ減らそうとしても、完全にゼロにはなりません。
ベンチマークは並列であっても、シーケンシャルであるかのように見えることがあります。この[estone?]ベンチマークは実際には、完全に直列実行になっています。そのため、リングベンチマークの一般的な実装では、一つのプロセスがアクティブである場合には他のプロセスは文を受け取るまでは待っていることになります。
並列性に関する潜在能力がどの程度あるかや、スケールしないボトルネックがどれだけあるかについては、プロファイルを使用することで、そのアプリケーションの感覚を得ることができます。
Copyright c 1991-2009 Ericsson AB