私は、大量の同時ユーザーを同時にメモリに表示できるシステムを設計しようとしています。このシステムの設計に着手したとき、私はすぐに、Erlang に似た何らかのアクター ベースのソリューションを思いつきました。
システムは .NET で実行する必要があるため、MailboxProcessor を使用して F# でプロトタイプの作業を開始しましたが、重大なパフォーマンスの問題に遭遇しました。私の最初のアイデアは、ユーザーごとに 1 つのアクター (MailboxProcessor) を使用して、1 人のユーザーの通信をシリアル化することでした。
私が見ている問題を再現する小さなコードを分離しました:
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 で約 800 ミリ秒かかります。次に、UpdateName
各アクターにメッセージを送信し、完了するまで約 1.8 秒かかります。
ここで、すべてのキューからのオーバーヘッドがあることに気付きました: ThreadPool での処理、AutoResetEvents の設定/リセットなど、MailboxProcessor の内部で。しかし、これは本当に期待通りのパフォーマンスでしょうか? 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 操作を削除し、すべてのユーザーに同じ名前を使用するだけでも、操作を実行するのに約 400 ミリ秒かかり、非常に遅く感じます。