在产卵和查杀过程中,F#真的比Erlang快吗?
更新:这个问题包含一个错误,使基准没有意义。 我将尝试一个比较F#和Erlang的基本并发功能的更好的基准,并在另一个问题中查询结果。
我试图理解Erlang和F#的性能特点。 我发现Erlang的并发模型非常吸引人,但我倾向于使用F#来实现互操作性。 尽管开箱即用的F#没有提供像Erlang的并发基元这样的东西,但从我所知道的异步和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#,我试图用异步和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#如何实现异步和MailBoxProcessor,但这些结果令人鼓舞。 我有什么可怕的错误吗?
如果没有,Erlang有可能会超越F#吗? 是否有任何理由Erlang的并发原语不能通过库带到F#中?
编辑:上述数字是错误的,由于错误布赖恩指出。 当我修复它时,我会更新整个问题。
在您的原始代码中,您只启动了一个MailboxProcessor。 使wait()
成为函数,并在每个yield
调用它。 此外,您并未等待他们旋转或接收信息,我认为这会使时间信息无效; 看到我的代码如下。
这就是说,我取得了一些成功; 在我的盒子上,我可以在大约25us的时候做10万次。 再过多之后,我认为你可能会像任何事情一样对分配器/ GC进行反击,但我也能做到一百万(每个约27us,但此时使用的是1.5G内存)。
基本上每个'暂停异步'(这是当一个邮箱正在等待一条线的状态
let! msg = inbox.Receive()
)在被阻塞时只占用一些字节数。 这就是为什么你可以拥有比线程更多异步的方式; 一个线程通常需要大于或等于兆字节的内存。
好的,这是我正在使用的代码。 你可以使用一个像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你可以找到有趣的实际比赛的结果。
链接地址: http://www.djcxy.com/p/43191.html上一篇: Is F# really faster than Erlang at spawning and killing processes?