在产卵和查杀过程中,F#真的比Erlang快吗?

更新:这个问题包含一个错误,使基准毫无意义。 我将尝试一个比较F#和Erlang的基本并发function的更好的基准,并在另一个问题上询问结果。

我正在尝试去理解Erlang和F#的性能特点。 我发现Erlang的并发模型非常吸引人,但是由于互操作性的原因,我倾向于使用F#。 尽pipe开箱即用的F#并没有提供像Erlang的并发原语这样的东西 – 从我所知道的asynchronous和MailboxProcessor只涵盖了Erlang很好的一小部分 – 我一直在试图理解F#中可能的性能明智的。

在Joe Armstrong编程的Erlang书中,他指出在Erlang中进程非常便宜。 他使用(大致)下面的代码来certificate这个事实:

-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微秒。 我可以进一步提高进程的数量,但是一百万人似乎总是把事情打破。

知道很less的F#,我试图用asynchronous和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 / processor的时间都在2微秒以内,比Erlang快大约4倍。 更重要的是,也许我可以扩展到数百万个进程而没有任何明显的问题。 每个进程启动1或2百万个进程仍然需要大约2微秒。 启动2000万个处理器仍然可行,但每个进程的速度减慢到6微秒左右。

我还没有花时间去完全理解F#如何实现asynchronous和MailBoxProcessor,但是这些结果是令人鼓舞的。 有什么我可怕的错误吗?

如果没有,Erlang有可能会超越F#吗? 是否有任何理由Erlang的并发原语不能通过库带到F#?

编辑:上面的数字是错误的,由于错误布赖恩指出。 我将在修复时更新整个问题。

在您的原始代码中,您只启动了一个MailboxProcessor。 使wait()函数,并与每个yield调用它。 你也不等他们旋转或接收消息,我认为使时间信息无效; 看到我的代码如下。

这就是说,我取得了一些成功。 在我的包厢里,我可以在25us左右做10万人。 再多了之后,我觉得可能你开始和分配器/ GC做任何事情了,但是我也能做到一百万(每次大约27us,但是现在使用的是1.5G的内存)。

基本上,每个“暂停asynchronous”(这是一个邮箱正在等待的状态,如

 let! msg = inbox.Receive() 

)在被阻塞时只占用一些字节数。 这就是为什么你可以拥有比线程更多asynchronous的方式。 一个线程通常需要大于或等于兆字节的内存。

好的,这是我正在使用的代码。 你可以使用一个像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#(虽然这是非常习惯的)。

Erlang的虚拟机不使用操作系统线程或进程切换到新的Erlang进程。 它只是将函数调用计入您的代码/进程,并在一些(进入相同的OS进程和相同的OS线程)之后跳转到其他VM的进程。

CLR使用基于OS进程和线程的机制,所以F#对每个上下文切换都有更高的开销成本。

所以回答你的问题是“不,Erlang比产卵和杀死过程快得多”。

PS你可以find有趣的实际比赛的结果 。