2013-06-28 14 views
7

私は同時ユーザーの大規模な量を同時にメモリに表現することを可能にするシステムを設計しようとしています。このシステムを設計するために出発するとき、私は直ちにアーランに何らかのアクターベースの解決策を考えました。MailboxProcessorのパフォーマンスの問題

システムは、.NETで行う必要がありますので、私はMailboxProcessorを使用してF#でプロトタイプに取り組んで始めたが、それらの深刻なパフォーマンスの問題に実行しています。私の最初のアイデアは、1人のユーザーの通信をシリアル化するために、ユーザーごとに1人の俳優(MailboxProcessor)を使用することでした。

open System.Threading; 
open System.Diagnostics; 

type Inc() = 

    let mutable n = 0; 
    let sw = new Stopwatch() 

    member x.Start() = 
     sw.Start() 

    member x.Increment() = 
     if Interlocked.Increment(&n) >= 100000 then 
      printf "UpdateName Time %A" sw.ElapsedMilliseconds 

type Message 
    = UpdateName of int * string 

type User = { 
    Id : int 
    Name : string 
} 

[<EntryPoint>] 
let main argv = 

    let sw = Stopwatch.StartNew() 
    let incr = new Inc() 
    let mb = 

     Seq.initInfinite(fun id -> 
      MailboxProcessor<Message>.Start(fun inbox -> 

       let rec loop user = 
        async { 
         let! m = inbox.Receive() 

         match m with 
         | UpdateName(id, newName) -> 
          let user = {user with Name = newName}; 
          incr.Increment() 
          do! loop user 
        } 

       loop {Id = id; Name = sprintf "User%i" id} 
      ) 
     ) 
     |> Seq.take 100000 
     |> Array.ofSeq 

    printf "Create Time %i\n" sw.ElapsedMilliseconds 
    incr.Start() 

    for i in 0 .. 99999 do 
     mb.[i % mb.Length].Post(UpdateName(i, sprintf "User%i-UpdateName" i)); 

    System.Console.ReadLine() |> ignore 

    0 

ちょうど100kの俳優が私のクワッドコアi7の上で周り800ms取る作成:

私はこの問題、私は見ていますを再現するコードの小片を単離しました。次に、俳優のそれぞれにUpdateNameメッセージを送信し、俳優の完了を待つのに約1.8秒かかります。

は今、私はすべてのキューからオーバーヘッドが実現:MailboxProcessorの内部など、AutoResetEventsをリセット/設定、ThreadPoolの上INGの。しかし、これは本当に期待されるパフォーマンスですか? MailboxProcessorにMSDNや様々なブログの両方を読んでから、私はそれはErlangの俳優に親族ことだという考えを得ているが、abyssmalパフォーマンスから私は、これが現実に成立していないようです見ていますか?

また、8つのMailboxProcessorsを使用するコードの修正版を試してみましたが、それぞれがMap<int, User>マップを保持していて、IDでユーザーを検索すると、UpdateName操作の合計時間が短縮されました1.2秒。しかし、それはまだ非常に遅いと感じ、変更されたコードはここにあります:

open System.Threading; 
open System.Diagnostics; 

type Inc() = 

    let mutable n = 0; 
    let sw = new Stopwatch() 

    member x.Start() = 
     sw.Start() 

    member x.Increment() = 
     if Interlocked.Increment(&n) >= 100000 then 
      printf "UpdateName Time %A" sw.ElapsedMilliseconds 

type Message 
    = CreateUser of int * string 
    | UpdateName of int * string 

type User = { 
    Id : int 
    Name : string 
} 

[<EntryPoint>] 
let main argv = 

    let sw = Stopwatch.StartNew() 
    let incr = new Inc() 
    let mb = 

     Seq.initInfinite(fun id -> 
      MailboxProcessor<Message>.Start(fun inbox -> 

       let rec loop users = 
        async { 
         let! m = inbox.Receive() 

         match m with 
         | CreateUser(id, name) -> 
          do! loop (Map.add id {Id=id; Name=name} users) 

         | UpdateName(id, newName) -> 
          match Map.tryFind id users with 
          | None -> 
           do! loop users 

          | Some(user) -> 
           incr.Increment() 
           do! loop (Map.add id {user with Name = newName} users) 
        } 

       loop Map.empty 
      ) 
     ) 
     |> Seq.take 8 
     |> Array.ofSeq 

    printf "Create Time %i\n" sw.ElapsedMilliseconds 

    for i in 0 .. 99999 do 
     mb.[i % mb.Length].Post(CreateUser(i, sprintf "User%i-UpdateName" i)); 

    incr.Start() 

    for i in 0 .. 99999 do 
     mb.[i % mb.Length].Post(UpdateName(i, sprintf "User%i-UpdateName" i)); 

    System.Console.ReadLine() |> ignore 

    0 

私の質問はここにあります、私は何か間違っていますか? MailboxProcessorをどのように使用するのか分からなかったのですか?または、このパフォーマンスが期待されるものです。

更新:

だから私はsprintfのを使用することが非常に遅いです、そして、それは結局のところ場所なことを私に伝えている、irc.freenode.net @ fsharp ##の上にいくつかの男のホールドを得ました私のパフォーマンスの問題の大部分はから来ていた。しかし、上記のsprintf操作を削除して、すべてのユーザーに同じ名前を使用するだけで、私はまだ実際に遅く感じる作業を約400msで終了します。

+3

sprintfが遅い場合は、生産システムでかなりのパフォーマンス/ –

+2

でパフォーマンスを向上させる可能性がある新しいF#3.1を試すことができます。ユーザーごとにエージェントを起動する可能性が高くなります。同様のメモで、すべてのユーザーが同時にメッセージを投稿すると思われますか?ルックアップアクセスが 'Seq.initInfinite'を使用して、エージェント –

+2

あなたは 'Array.init'を使っていましたが、これはあなたの作成時間コストの一部として表示されます。 –

答えて

2

sprintfを排除した後、私は約12秒(Macのモノはその速くない)を得ました。 Phil TrelfordがMapの代わりにDictionaryを使用すると、600msになりました。 Win/.Netで試したことがありません。

コードの変更は十分に簡単で、地元の可変性は私に完全に受け入れられる:

let mb = 
    Seq.initInfinite(fun id -> 
     MailboxProcessor<Message>.Start(fun inbox -> 
      let di = System.Collections.Generic.Dictionary<int,User>() 
      let rec loop() = 
       async { 
        let! m = inbox.Receive() 

        match m with 
        | CreateUser(id, name) -> 
         di.Add(id, {Id=id; Name=name}) 
         return! loop() 

        | UpdateName(id, newName) -> 
         match di.TryGetValue id with 
         | false, _ -> 
          return! loop() 

         | true, user -> 
          incr.Increment() 
          di.[id] <- {user with Name = newName} 
          return! loop() 
       } 

      loop() 
     ) 
    ) 
    |> Seq.take 8 
    |> Array.ofSeq 
+3

'return!'ではなく 'do!'を使って非同期ワークフローでテールを繰り返すか、(ヒープ割り当てされた)スタックフレームをリークさせる必要があります! –

+0

@JonHarropはい!答えを編集しました。ありがとう。 –

14

今、私はすべてのキューからオーバーヘッドが実現:ThreadPoolの上INGの、/リセットAutoResetEventsを設定しますMailboxProcessorの内部で使用されます。

そしてprintfMapSeqとグローバル可変Incを争います。また、ヒープ割り当てスタックフレームが漏れています。実際、ベンチマークを実行するのに費やされる時間のほんの一部はMailboxProcessorと関係があります。

これは本当に予想されるパフォーマンスですか?

私はあなたのプログラムのパフォーマンスに驚くことはありませんが、MailboxProcessorのパフォーマンスについてはあまり言及していません。

MailboxProcessorにMSDNや様々なブログの両方を読んでから、私はそれはErlangの俳優に親族ことだという考えを得ているが、abyssmalパフォーマンスから私は、これが現実に成立していないようです見ていますか?

MailboxProcessorは、概念的に、アーランの一部に似ています。あなたが見ている過酷なパフォーマンスは様々なものが原因です。その中のいくつかは非常に微妙で、そのようなプログラムに影響します。

私の質問は間違っていますか?

あなたはいくつか間違っていると思います。まず、解決しようとしている問題は明確ではないので、これはXY problem質問のように聞こえます。次に、間違ったものをベンチマークしようとしています(たとえば、MailboxProcessorを作成するために必要なマイクロ秒の時間について不平を言っていますが、TCP接続が確立されてから数桁の時間がかかる場合のみそうすることがあります)。第3に、あなたはいくつかのことのパフォーマンスを測定するベンチマークプログラムを書いていますが、あなたの観察は全く異なるものに帰されています。

あなたのベンチマークプログラムをより詳しく見てみましょう。他に何かをする前に、いくつかのバグを修正しましょう。より正確であるため、時間を測定するには常にsw.Elapsed.TotalSecondsを使用する必要があります。 return!を使用し、do!ではなく常に非同期ワークフローで再発する必要があります。そうしないと、スタックフレームがリークします。

私の最初のタイミングは以下のとおりです。

Creation stage: 0.858s 
Post stage: 1.18s 

次は、私たちのプログラムは、実際のF#MailboxProcessorをスラッシングそのほとんどの時間を費やしていることを確認するために、プロファイルを実行してみましょう:

77% Microsoft.FSharp.Core.PrintfImpl.gprintf(...) 
4.4% Microsoft.FSharp.Control.MailboxProcessor`1.Post(!0) 

明らかではない私たちは、期待していた。もっと抽象的に考えると、我々はsprintfのようなものを使ってたくさんのデータを生成していますが、それを適用していますが、私たちは世代とアプリケーションを一緒にしています。私たちの初期化コードを分離してみましょう:

let ids = Array.init 100000 (fun id -> {Id = id; Name = sprintf "User%i" id}) 
... 
    ids 
    |> Array.map (fun id -> 
     MailboxProcessor<Message>.Start(fun inbox -> 
... 
      loop id 
... 
    printf "Create Time %fs\n" sw.Elapsed.TotalSeconds 
    let fxs = 
     [|for i in 0 .. 99999 -> 
      mb.[i % mb.Length].Post, UpdateName(i, sprintf "User%i-UpdateName" i)|] 
    incr.Start() 
    for f, x in fxs do 
     f x 
... 

は今我々が得る:

Creation stage: 0.538s 
Post stage: 0.265s 

ので作成が60%高速で、投稿は4.5X高速です。

はのは、完全にあなたのベンチマークを書き換えてみましょう:そのエージェントが大幅にその共有カウンタのパフォーマンスへの影響を軽減、共有カウンタをインクリメントする前に、

do 
    for nAgents in [1; 10; 100; 1000; 10000; 100000] do 
    let timer = System.Diagnostics.Stopwatch.StartNew() 
    use barrier = new System.Threading.Barrier(2) 
    let nMsgs = 1000000/nAgents 
    let nAgentsFinished = ref 0 
    let makeAgent _ = 
     new MailboxProcessor<_>(fun inbox -> 
     let rec loop n = 
      async { let!() = inbox.Receive() 
        let n = n+1 
        if n=nMsgs then 
        let n = System.Threading.Interlocked.Increment nAgentsFinished 
        if n = nAgents then 
         barrier.SignalAndWait() 
        else 
        return! loop n } 
     loop 0) 
    let agents = Array.init nAgents makeAgent 
    for agent in agents do 
     agent.Start() 
    printfn "%fs to create %d agents" timer.Elapsed.TotalSeconds nAgents 
    timer.Restart() 
    for _ in 1..nMsgs do 
     for agent in agents do 
     agent.Post() 
    barrier.SignalAndWait() 
    printfn "%fs to post %d msgs" timer.Elapsed.TotalSeconds (nMsgs * nAgents) 
    timer.Restart() 
    for agent in agents do 
     use agent = agent 
    () 
    printfn "%fs to dispose of %d agents\n" timer.Elapsed.TotalSeconds nAgents 

このバージョンでは、各エージェントにnMsgsを期待しています。このプログラムはまた、エージェントの数が異なるとパフォーマンスを調べます。私が手にこのマシン上:

Agents M msgs/s 
    1 2.24 
    10 6.67 
    100 7.58 
    1000 5.15 
10000 1.15 
100000 0.36 

だから、下のMSG /秒の理由の一部は、あなたが見ているスピードと思われる物質の異常-多数(10万)です。 10〜1,000のエージェントでは、F#の実装は10万倍以上、100,000のエージェントよりも高速です。

ですから、パフォーマンスのこの種で間に合わせることができれば、あなたはF#でアプリケーション全体を記述することができるはずですが、あなたははるかにパフォーマンスをEEKする必要がある場合、私は別のアプローチを使用することをお勧めします。あなたも、F#を使用して犠牲にする必要はないかもしれません(そして、あなたは確かにプロトタイピングのためにそれを使用することができます)かく乱のようなデザインを採用することで。実際には、私は、.NET上で連載をやって過ごした時間は、F#の非同期とMailboxProcessorで過ごした時間よりもはるかに大きくなる傾向があることを見出しました。