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

问题描述:

更新:这个问题包含一个错误,使基准毫无意义。我将尝试一个比较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#,开始和杀害100000名演员/处理器下每个进程2微秒花费,比二郎神快约4倍。更重要的是,也许,我可以扩展到数百万个进程而没有任何明显的问题。每个进程启动1或2百万个进程仍需要大约2微秒。启动2000万个处理器仍然可行,但每个进程的速度减慢到6微秒左右。

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

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

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

+1

+1对于真正高级的问题。 – gahooa 2010-02-06 22:50:37

+0

+1也很有趣,不仅仅是先进的。 – gradbot 2010-02-06 23:12:26

+1

您是否使用Erlang的“erl + native”选项? (请参阅http://*.com/questions/2207451/erlang-compilation-mixed-of-hipe-object-code-and-opcode) – jldupont 2010-02-07 13:38:28

在您的原始代码中,您只启动了一个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 

这一切说,我不知道二郎,我还没有深入想过是否有修剪下来的F#任何更多的(虽然这是很地道的不变化)的方法。

+0

呃哦。我怀疑我可能会犯这样的错误。 – Tristan 2010-02-06 23:16:57

+0

只是为了确认,我应该用wait()替换两个等待?在500微秒的范围内,我得到的结果要差得多。我没有消化真正发生的事情,所以你的第二点可能是更大的问题。 – Tristan 2010-02-06 23:34:53

+0

上面的代码给了我每个进程大约1000微秒,这比Erlang差100倍。但我不确定这两个例子是否相同。减少一个可变变量看起来非常不Erlang。我的理解是,演员只应该通过信息交谈。 – Tristan 2010-02-07 00:03:36

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

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

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

P.S.你可以找到results of that practical contest有趣。

+0

这就是我的想法,这就是为什么我发现我的结果令人困惑。但他们错了。然而,这是一种内在的限制,还是目前设计的方式?我没有完全看它,但单线程异步描述在这里:http://cs.hubfs.net/blogs/hell_is_other_languages/archive/2008/08/03/6506.aspx – Tristan 2010-02-07 07:31:20

+0

这是用户空间线程很好的例子。您可以看到F#的源代码,他们的异步使用CLR ThreadPool。 – ssp 2010-02-07 08:09:56

+8

F#asyncs是用户空间线程。他们没有与OS线程相关的开销。他们可以使用线程池来利用多个内核,但从来没有比硬件本身支持更多的线程,并且常见类型的上下文切换保留在单个OS线程内。使用单一(非超线程)核心,它们纯粹是用户空间线程,如Erlang。 – RD1 2010-04-28 15:39:59