2013-06-28 156 views
7

我一直在試圖設計一個系統,它允許大量的併發用戶同時在內存中表示。當開始設計這個系統時,我立刻想到了某種基於演員的解決方案,即Erlang的一個親屬。郵箱處理器性能問題

該系統必須在.NET中完成,因此我開始使用MailboxProcessor在F#中處理原型,但遇到了嚴重的性能問題。我最初的想法是爲每個用戶使用一個actor(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秒。

現在,我意識到所有隊列都有開銷:在ThreadPool上,在MailboxProcessor內部設置/重置AutoResetEvents等。但這真的是預期的表現嗎?從閱讀MSDN和MailboxProcessor上的各種博客,我都明白這是erlang演員的親戚,但是從我看到的這種深淵表現看來,這似乎並不適用於現實?

我也嘗試了代碼,它使用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應該如何使用?或者這是預期的表現。

更新:

所以我得到了## fsharp @ irc.freenode.net,它告訴我,使用的sprintf很慢一些球員的保持,事實證明這是一個地方我的大部分表現問題都來自於。但是,刪除上面的sprintf操作,併爲每個用戶使用相同的名稱,我仍然最終得到大約400ms的操作,這感覺非常慢。

+3

如果sprintf速度很慢 - 您可以嘗試新的F#3.1,這顯然可以提高生產系統的性能達到顯着水平/ –

+2

也許您更有可能按需啓動代理,這可能意味着啓動時間這不是一個大問題,在類似的筆記中,你是否期望所有用戶同時發佈消息? –

+2

您還可以通過使用一個可變的字典在一個不變的地圖,爲查找訪問是通過代理 –

回答

2

消除sprintf後,我得到了約12秒(在Mac Mono是不是快)。 考慮使用,而不是地圖詞典的菲爾Trelford的建議,但去了600毫秒。在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

必須尾巴在異步工作流程使用'返回復發!'和'沒有做!'或者你會泄漏(堆上分配的)堆棧幀! –

+0

@JonHarrop是的!編輯答案。謝謝。 –

14

現在,我知道有開銷從所有隊列:荷蘭國際集團的線程池,設置/復位AutoResetEvents等內部在MailboxProcessor中。

而且printfMapSeq和爭奪全局可變Inc。而你正在泄漏堆分配的堆棧幀。事實上,只有運行基準測試所花費的時間的一小部分有什麼關係MailboxProcessor

但這真的預期的性能?

我不是你的程序的性能感到驚訝,但它並沒有說太多關於MailboxProcessor性能。

從閱讀這兩種MSDN和對MailboxProcessor各種博客我已經得到的想法,它是一個親屬二郎神角色,但是從abyssmal表現我看到這個似乎不抱在現實中真的嗎?

MailboxProcessor在概念上有點類似於Erlang的一部分。你看到的糟糕表現是由於各種各樣的事情,其中​​一些非常微妙並且會影響任何這樣的節目。

所以我的問題是在這裏,我是不是做錯了什麼?

我認爲你做的幾件事情是錯誤的。首先,你試圖解決的問題並不清楚,所以這聽起來像一個XY problem問題。其次,你想基準錯誤的東西(如你所抱怨創建MailboxProcessor需要微秒的時間,但可能打算在建立TCP連接這需要幾個數量級的更長的時間才能做到只)。第三,你寫了一個基準程序來衡量一些事情的表現,但是把你的觀察歸因於完全不同的事情。

讓我們看看你的基準測試程序的更多細節。在我們做其他事情之前,讓我們來修復一些錯誤。您應該始終使用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.5倍速度更快。

讓我們嘗試徹底改寫你的風向標:

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 

所以它出現的原因下封郵件/ s的速度,你看到的那部分代理商的異常,大量的(100,000)。使用10-1000個代理,F#實現速度比使用100,000個代理的速度快10倍以上。

所以,如果你可以用這樣的表現做,那麼你應該能夠編寫在F#你的整個應用程序,但如果你需要伊克出更多的表現我會建議使用一種不同的方法。採用像Disruptor這樣的設計,你甚至可以不必犧牲使用F#(並且你可以使用它來進行原型設計)。在實踐中,我發現在.NET上進行序列化的時間往往比在F#異步和MailboxProcessor上花費的時間大得多。