更新:此问题包含一个错误,使基准无意义。我将尝试更好的基准比较F#和Erlang的基本并发功能,并在另一个问题中查询结果。
我正在尝试了解Erlang和F#的性能特征。我发现Erlang的并发模型非常吸引人,但我倾向于使用F#来实现互操作性。虽然开箱即用F#没有提供像Erlang的并发原语 - 从我可以告诉async和MailboxProcessor仅涵盖Erlang做得很好的一小部分 - 我一直试图了解F#性能的可能性明智的。
在Joe Armstrong编写的Erlang编程书中,他指出Erlang中的进程非常便宜。他使用(大致)以下代码来证明这一事实:
-module(processes).
-export([max/1]).
%% max(N)
%% Create N processes then destroy them
%% See how much time this takes
max(N) ->
statistics(runtime),
statistics(wall_clock),
L = for(1, N, fun() -> spawn(fun() -> wait() end) end),
{_, Time1} = statistics(runtime),
{_, Time2} = statistics(wall_clock),
lists:foreach(fun(Pid) -> Pid ! die end, L),
U1 = Time1 * 1000 / N,
U2 = Time2 * 1000 / N,
io:format("Process spawn time=~p (~p) microseconds~n",
[U1, U2]).
wait() ->
receive
die -> void
end.
for(N, N, F) -> [F()];
for(I, N, F) -> [F()|for(I+1, N, F)].
在我的Macbook Pro上,每个进程产生并杀死10万个进程(processes:max(100000)
)大约需要8微秒。我可以进一步提高进程的数量,但是一百万似乎可以非常一致地破坏它。
知道很少的F#,我试图使用async和MailBoxProcessor来实现这个例子。我的尝试可能是错误的,如下:
#r "System.dll"
open System.Diagnostics
type waitMsg =
| Die
let wait =
MailboxProcessor.Start(fun inbox ->
let rec loop =
async { let! msg = inbox.Receive()
match msg with
| Die -> return() }
loop)
let max N =
printfn "Started!"
let stopwatch = new Stopwatch()
stopwatch.Start()
let actors = [for i in 1 .. N do yield wait]
for actor in actors do
actor.Post(Die)
stopwatch.Stop()
printfn "Process spawn time=%f microseconds." (stopwatch.Elapsed.TotalMilliseconds * 1000.0 / float(N))
printfn "Done."
在Mono上使用F#,每个进程启动和杀死100,000个actor /处理器的时间不到2微秒,大约比Erlang快4倍。更重要的是,也许是我可以扩展到数百万个流程而没有任何明显的问题。每个进程启动1或2百万个进程仍然需要大约2微秒。启动2000万个处理器仍然可行,但每个进程的速度减慢到大约6微秒。
我还没有花时间完全理解F#如何实现异步和MailBoxProcessor,但这些结果令人鼓舞。我有什么可怕的错误吗?
如果没有,是否有一些地方Erlang可能会胜过F#?是否有任何理由不能通过库将Erlang的并发原语带到F#中?
编辑:由于Brian指出的错误,上述数字是错误的。我会在修复时更新整个问题。
答案 0 :(得分:23)
在原始代码中,您只启动了一个MailboxProcessor。将wait()
设为一个函数,并使用每个yield
调用它。此外,您不是在等待它们启动或接收消息,我认为这会使时序信息无效;请参阅下面的代码。
那就是说,我取得了一些成功;在我的盒子上,每个约25us,我可以做到100,000。经过更多的努力,我想你可能会开始与分配器/ GC一样多,但我也能做到一百万(每个大约27us,但此时使用的是1.5G的内存)。
基本上每个'暂停异步'(这是邮箱在
等行上等待的状态let! msg = inbox.Receive()
)在被阻止时只占用一些字节数。这就是为什么你可以拥有比线程更多的asyncs方式,方式,方式;一个线程通常需要一兆字节或更多的内存。
好的,这是我正在使用的代码。您可以使用像10这样的小数字,并使用--define DEBUG来确保程序语义是所期望的(printf输出可能是交错的,但你会得到这个想法)。
open System.Diagnostics
let MAX = 100000
type waitMsg =
| Die
let mutable countDown = MAX
let mre = new System.Threading.ManualResetEvent(false)
let wait(i) =
MailboxProcessor.Start(fun inbox ->
let rec loop =
async {
#if DEBUG
printfn "I am mbox #%d" i
#endif
if System.Threading.Interlocked.Decrement(&countDown) = 0 then
mre.Set() |> ignore
let! msg = inbox.Receive()
match msg with
| Die ->
#if DEBUG
printfn "mbox #%d died" i
#endif
if System.Threading.Interlocked.Decrement(&countDown) = 0 then
mre.Set() |> ignore
return() }
loop)
let max N =
printfn "Started!"
let stopwatch = new Stopwatch()
stopwatch.Start()
let actors = [for i in 1 .. N do yield wait(i)]
mre.WaitOne() |> ignore // ensure they have all spun up
mre.Reset() |> ignore
countDown <- MAX
for actor in actors do
actor.Post(Die)
mre.WaitOne() |> ignore // ensure they have all got the message
stopwatch.Stop()
printfn "Process spawn time=%f microseconds." (stopwatch.Elapsed.TotalMilliseconds * 1000.0 / float(N))
printfn "Done."
max MAX
所有这一切都说,我不知道Erlang,我还没有深入思考是否有办法减少F#(尽管它非常惯用)。
答案 1 :(得分:15)
Erlang的VM不使用操作系统线程或进程切换到新的Erlang进程。它的VM只是将函数调用计入代码/进程,然后跳转到其他VM的进程(进入相同的OS进程和相同的OS线程)。
CLR使用基于操作系统进程和线程的机制,因此F#对每个上下文切换都有更高的开销成本。
所以回答你的问题是“不,Erlang比产生和杀死进程快得多”。
P.S。你可以找到有趣的results of that practical contest。