I have been trying to design a system which allows a large amount of concurrent users to be represented in memory at the same time. When setting out to design this sytem I immediately thought of some sort of actor based solution a kin to Erlang.
The system has to be done in .NET, so I started working on a prototype in F# using MailboxProcessor but have run into serious performance problems with them. My initial idea was to use one actor (MailboxProcessor) per user to serialize communication the communication for one user.
I have isolated a small piece of code that reproduces the problem I am seeing:
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
Just creating the 100k actors take around 800ms on my quad core i7. Then submitting the UpdateName
message to each one of the actor and wait for them to complete takes about 1.8 seconds.
Now, I realize there is overhead from all the queue:ing on the ThreadPool, setting/resetting AutoResetEvents, etc internally in the MailboxProcessor. But is this really the expected performance? From reading both MSDN and various blogs on the MailboxProcessor I have gotten the idea that it's to be a kin to erlang actors, but from the abyssmal performance I am seeing this doesn't seem to hold true in reality?
I also tried a modified version of the code, which uses 8 MailboxProcessors and each one of them hold a Map<int, User>
map which is used to lookup a user by id, it yielded some improvements bringing down the total time for the UpdateName operation to 1.2 seconds. But it still feels very slow, the modified code is here:
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
So my question is here, am I doing something wrong? Have I missunderstood how the MailboxProcessor is supposed to be used? Or is this performance what is expected.
Update:
So I got a hold of some guys on ##fsharp @ irc.freenode.net, which informed me that using sprintf is very slow, and as it turns out that is where a large part of my performance problems were comming from. But, removing the sprintf operations above and just using the same name for every User, I still end up with about 400ms for doign the operations, which feels really slow.