gpt4 book ai didi

.net - MailboxProcessor 性能问题

转载 作者:行者123 更新时间:2023-12-04 03:04:26 27 4
gpt4 key购买 nike

我一直在尝试设计一个允许大量并发用户同时在内存中表示的系统。在着手设计这个系统时,我立即想到了某种基于 Actor 的解决方案,类似于 Erlang。

该系统必须在 .NET 中完成,因此我开始使用 MailboxProcessor 在 F# 中制作原型(prototype),但遇到了严重的性能问题。我最初的想法是为每个用户使用一个参与者(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

在我的四核 i7 上创建 100k 个 Actor 大约需要 800 毫秒。然后提交 UpdateName向每个参与者发送消息并等待他们完成大约需要 1.8 秒。

现在,我意识到所有队列都有开销:在 MailboxProcessor 内部的 ThreadPool、设置/重置 AutoResetEvents 等。但这真的是预期的表现吗?通过阅读 MSDN 和 MailboxProcessor 上的各种博客,我了解到它与 erlang Actor 类似,但从我看到的糟糕表现来看,这在现实中似乎并不成立?

我还尝试了代码的修改版本,它使用了 8 个 MailboxProcessor,每个都有一个 Map<int, User> map 用于通过 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 毫秒来执行操作,这感觉真的很慢。

最佳答案

Now, I realize there is overhead from all the queue:ing on the ThreadPool, setting/resetting AutoResetEvents, etc internally in the MailboxProcessor.



printf , Map , Seq并争夺你的全局可变 Inc .而且您正在泄漏堆分配的堆栈帧。事实上,运行基准测试所花费的时间中只有一小部分与 MailboxProcessor 有任何关系。 .

But is this really the expected performance?



我对您的程序的性能并不感到惊讶,但它并没有说明 MailboxProcessor 的性能。 .

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?


MailboxProcessor在概念上有点类似于 Erlang 的一部分。您看到的糟糕表现是由多种原因造成的,其中一些非常微妙,会影响任何此类程序。

So my question is here, am I doing something wrong?



我认为你做错了一些事情。首先,您要解决的问题不清楚,所以这听起来像是 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

因此,您看到的 msgs/s 速度较低的部分原因似乎是代理数量异常多(100,000)。使用 10-1,000 个代理时,F# 实现的速度比使用 100,000 个代理时快 10 倍以上。

因此,如果您可以使用这种性能,那么您应该能够在 F# 中编写整个应用程序,但如果您需要获得更高的性能,我建议您使用不同的方法。通过采用像 Disruptor 这样的设计,您甚至可能不必牺牲使用 F#(当然您可以将它用于原型(prototype)设计)。在实践中,我发现在 .NET 上进行序列化所花费的时间往往比在 F# async 和 MailboxProcessor 上花费的时间要长得多。 .

关于.net - MailboxProcessor 性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17360286/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com