web-dev-qa-db-ja.com

F#は、プロセスの生成と強制終了においてErlangよりも本当に高速ですか?

更新:この質問には、ベンチマークを無意味にするエラーが含まれています。F#とErlangの基本的な同時実行機能を比較して、より良いベンチマークを試み、別の質問で結果について問い合わせます。

ErlangとF#のパフォーマンス特性を理解しようとしています。 Erlangの並行性モデルは非常に魅力的ですが、相互運用性の理由からF#を使用する傾向があります。箱から出してF#はErlangの同時実行プリミティブのようなものを提供していませんが(非同期と言うことができ、MailboxProcessorはErlangがうまくいくことのごく一部しかカバーしていません)、F#のパフォーマンスで何が可能かを理解しようとしています賢い。

JoeArmstrongのProgrammingErlangの本で、彼はErlangのプロセスが非常に安いと指摘しています。彼は(大まかに)次のコードを使用して、この事実を示しています。

_-module(processes).
-export([max/1]).

%% max(N) 
%%   Create N processes then destroy them
%%   See how much time this takes

max(N) ->
    statistics(runtime),
    statistics(wall_clock),
    L = for(1, N, fun() -> spawn(fun() -> wait() end) end),
    {_, Time1} = statistics(runtime),
    {_, Time2} = statistics(wall_clock),
    lists:foreach(fun(Pid) -> Pid ! die end, L),
    U1 = Time1 * 1000 / N,
    U2 = Time2 * 1000 / N,
    io:format("Process spawn time=~p (~p) microseconds~n",
          [U1, U2]).

wait() ->
    receive
        die -> void
    end.

for(N, N, F) -> [F()];
for(I, N, F) -> [F()|for(I+1, N, F)].
_

私のMacbookProでは、10万個のプロセス(processes:max(100000))を生成して強制終了するには、プロセスごとに約8マイクロ秒かかります。プロセスの数をもう少し増やすことはできますが、100万はかなり一貫して物事を壊しているようです。

F#をほとんど知らないので、asyncとMailBoxProcessorを使用してこの例を実装しようとしました。間違っているかもしれない私の試みは次のとおりです。

_#r "System.dll"
open System.Diagnostics

type waitMsg =
    | Die

let wait =
    MailboxProcessor.Start(fun inbox ->
        let rec loop =
            async { let! msg = inbox.Receive()
                    match msg with 
                    | Die -> return() }
        loop)

let max N =
    printfn "Started!"
    let stopwatch = new Stopwatch()
    stopwatch.Start()
    let actors = [for i in 1 .. N do yield wait]
    for actor in actors do
        actor.Post(Die)
    stopwatch.Stop()
    printfn "Process spawn time=%f microseconds." (stopwatch.Elapsed.TotalMilliseconds * 1000.0 / float(N))
    printfn "Done."
_

MonoでF#を使用すると、100,000のアクター/プロセッサーを起動して強制終了するのにかかる時間はプロセスあたり2マイクロ秒未満で、Erlangの約4倍です。さらに重要なのは、おそらく、明らかな問題なしに数百万のプロセスにスケールアップできることです。 100万または200万のプロセスを開始するには、プロセスごとに約2マイクロ秒かかります。 2,000万個のプロセッサを起動することはまだ可能ですが、プロセスごとに約6マイクロ秒に遅くなります。

F#がasyncとMailBoxProcessorを実装する方法を完全に理解するのにまだ時間がかかりませんが、これらの結果は有望です。私がひどく間違っていることはありますか?

そうでない場合、ErlangがF#を上回る可能性が高い場所はありますか? Erlangの並行性プリミティブをライブラリを介してF#に持ち込めない理由はありますか?

編集:ブライアンが指摘したエラーのため、上記の数字は間違っています。修正したら質問全体を更新します。

61
Tristan

元のコードでは、1つのMailboxProcessorのみを開始しました。 wait()を関数にして、各yieldで呼び出します。また、あなたは彼らがスピンアップしたりメッセージを受信したりするのを待っていません。それはタイミング情報を無効にすると思います。以下の私のコードを参照してください。

そうは言っても、私にはある程度の成功があります。私の箱では、それぞれ約25usで100,000を実行できます。やりすぎた後は、アロケーター/ GCと何よりも戦い始めるかもしれませんが、私も100万を達成できました(それぞれ約27usですが、この時点では1.5Gのメモリを使用していました)。

基本的に、各「中断された非同期」(メールボックスが次のような行で待機しているときの状態)

let! msg = inbox.Receive()

)ブロックされている間は、数バイトしかかかりません。そのため、スレッドよりもはるかに多くの非同期を使用できます。スレッドは通常、1メガバイト以上のメモリを必要とします。

わかりました、これが私が使用しているコードです。 10のような小さな数を使用し、-define DEBUGを使用して、プログラムのセマンティクスが目的どおりであることを確認できます(printf出力はインターリーブされる場合がありますが、わかります)。

open System.Diagnostics 

let MAX = 100000

type waitMsg = 
    | Die 

let mutable countDown = MAX
let mre = new System.Threading.ManualResetEvent(false)

let wait(i) = 
    MailboxProcessor.Start(fun inbox -> 
        let rec loop = 
            async { 
#if DEBUG
                printfn "I am mbox #%d" i
#endif                
                if System.Threading.Interlocked.Decrement(&countDown) = 0 then
                    mre.Set() |> ignore
                let! msg = inbox.Receive() 
                match msg with  
                | Die -> 
#if DEBUG
                    printfn "mbox #%d died" i
#endif                
                    if System.Threading.Interlocked.Decrement(&countDown) = 0 then
                        mre.Set() |> ignore
                    return() } 
        loop) 

let max N = 
    printfn "Started!" 
    let stopwatch = new Stopwatch() 
    stopwatch.Start() 
    let actors = [for i in 1 .. N do yield wait(i)] 
    mre.WaitOne() |> ignore // ensure they have all spun up
    mre.Reset() |> ignore
    countDown <- MAX
    for actor in actors do 
        actor.Post(Die) 
    mre.WaitOne() |> ignore // ensure they have all got the message
    stopwatch.Stop() 
    printfn "Process spawn time=%f microseconds." (stopwatch.Elapsed.TotalMilliseconds * 1000.0 / float(N)) 
    printfn "Done." 

max MAX

そうは言っても、私はErlangを知りませんし、F#をこれ以上トリミングする方法があるかどうかについては深く考えていません(それはかなり慣用的なものですが)。

23
Brian

ErlangのVMは、OSスレッドやプロセスを使用して新しいErlangプロセスに切り替えることはありません。VMは、コード/プロセスへの関数呼び出しをカウントし、他のプロセスにジャンプします。いくつかの後のVMのプロセス(同じOSプロセスと同じOSスレッドへ)。

CLRはOSプロセスとスレッドに基づくメカニズムを使用するため、F#では各コンテキストスイッチのオーバーヘッドコストがはるかに高くなります。

したがって、あなたの質問に対する答えは、「いいえ、Erlangはプロセスを生成して強制終了するよりもはるかに高速です」です。

P.S. その実用的なコンテストの結果 興味深いことがわかります。

15
ssp