63

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

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

Joe Armstrong の Programming Erlang の本で、彼は 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)].

私の Macbook Pro では、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 マイクロ秒かかります。2000 万のプロセッサを起動することはまだ可能ですが、プロセスあたり約 6 マイクロ秒まで遅くなります。

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

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

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

4

2 に答える 2

24

元のコードでは、MailboxProcessor を 1 つだけ開始しました。関数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#をこれ以上削減する方法があるかどうかについても深く考えたことはありません(現状ではかなり慣用的ですが)。

于 2010-02-06T23:14:22.283 に答える
15

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

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

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

PSその実践的なコンテストの結果は興味深いものです。

于 2010-02-07T07:04:56.983 に答える