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

Tri*_*tan 61 concurrency erlang f# actor

更新:此问题包含一个错误,使基准无意义.我将尝试更好的基准比较F#和Erlang的基本并发功能,并在另一个问题中查询结果.

我正在尝试了解Erlang和F#的性能特征.我发现Erlang的并发模型非常吸引人,但我倾向于使用F#来实现互操作性.虽然开箱即用F#不提供像Erlang的并发原语 - 从我可以告诉async和MailboxProcessor只涵盖Erlang做得很好的一小部分 - 我一直试图了解F#性能的可能性明智的.

在Joe Armstrong的Programming 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)].
Run Code Online (Sandbox Code Playgroud)

在我的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."
Run Code Online (Sandbox Code Playgroud)

在Mono上使用F#,每个进程启动和杀死100,000个actor /处理器的时间不到2微秒,大约比Erlang快4倍.更重要的是,也许是我可以扩展到数百万个流程而没有任何明显的问题.每个进程启动1或2百万个进程仍然需要大约2微秒.启动2000万个处理器仍然可行,但每个进程的速度减慢到大约6微秒.

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

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

编辑:由于Brian指出的错误,上述数字是错误的.我修复它时会更新整个问题.

Bri*_*ian 23

在原始代码中,您只启动了一个MailboxProcessor.创建wait()一个函数,并与每个函数调用它yield.此外,您不是在等待它们启动或接收消息,我认为这会使时序信息无效; 看下面的代码.

那说,我有一些成功; 在我的盒子上,每个约25us,我可以做到100,000.经过更多的努力之后,我想你可能会开始与分配器/ GC一样多,但我也能做到一百万(每个大约27us,但此时使用的是1.5G的内存).

基本上每个'暂停异步'(这是邮箱在一条线上等待的状态

let! msg = inbox.Receive()
Run Code Online (Sandbox Code Playgroud)

)在被阻止时只占用一些字节数.这就是为什么你可以拥有比线程更多的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
Run Code Online (Sandbox Code Playgroud)

所有这些说,我不知道Erlang,我还没有深入思考是否有办法减少F#(尽管它是非常惯用的).


ssp*_*ssp 15

Erlang的VM不使用操作系统线程或进程来切换到新的Erlang进程.它的VM只是将函数调用计入代码/进程,然后跳转到其他VM的进程(进入相同的OS进程和相同的OS线程).

CLR使用基于OS进程和线程的机制,因此F#对于每个上下文切换具有更高的开销成本.

所以回答你的问题是"不,Erlang比产生和杀死进程快得多".

PS你可以找到有趣的实际比赛的结果.

  • F#asyncs是用户空间线程.它们没有与OS线程相关的开销.他们可以使用线程池来利用多个内核,但是本机上永远不会有硬件支持的线程,并且常见的上下文切换类型保留在单个OS线程中.使用单个(非超线程)核心,它们是纯粹的用户空间线程,如Erlang. (9认同)
  • @ssp 我在上面运行了 Brian 的代码,将异步中的所有线程 ID 添加到一个集合中并在最后打印它们。即使启动了 100,000 个代理(和异步),也恰好有 5 个线程。即使使用了线程池,调度也几乎完全在用户空间中完成。唯一的例外是如果您使用阻塞 I/O,则需要在工作线程被阻塞时补充线程池。但是,异步旨在与非阻塞 I/O 一起使用。我猜你的例子使用了阻塞 I/O?能发个代码链接吗? (2认同)
  • @ssp 是的,我试过你的代码 - 很有趣。看起来由于超时实现中的竞争条件而导致额外的线程阻塞。当我将它改为使用 Observable.Timeout 时,这些额外的线程被避免了(但它们所涉及的成本无论如何都很小)。所以,如果你的答案基于这个例子,那么我认为它根本没有代表性。通常异步任务在少量线程之间交换。 (2认同)