63

更新:此问题包含一个错误,导致基准测试毫无意义。我将尝试一个更好的基准来比较 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)].

在我的 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 个演员/处理器每个进程花费不到 2 微秒,大约比 Erlang 快 4 倍。也许更重要的是,我可以扩展到数百万个进程而不会出现任何明显的问题。启动 1 或 200 万个进程仍然需要每个进程大约 2 微秒。启动 2000 万个处理器仍然是可行的,但会减慢到每个进程大约 6 微秒。

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

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

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

4

2 回答 2

24

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

也就是说,我取得了一些成功;在我的盒子上,我可以在每个大约 25 美元的情况下完成 100,000 个。经过太多之后,我想你可能会开始尽可能多地与分配器/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#(尽管它按原样很惯用)。

于 2010-02-06T23:14:22.283 回答
15

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

CLR 使用基于 OS 进程和线程的机制,因此 F# 每次上下文切换的开销要高得多。

因此,您的问题的答案是“不,Erlang 比生成和杀死进程要快得多”。

PS 你会发现那个实际比赛的结果很有趣。

于 2010-02-07T07:04:56.983 回答