Es F # realmente más rápido que Erlang en los procesos de desove y matanza?


Actualizado: Esta pregunta contiene un error que hace que el punto de referencia carezca de sentido. Intentaré un mejor punto de referencia comparando F# y la funcionalidad de concurrencia básica de Erlang y preguntaré sobre los resultados en otra pregunta.

Estoy tratando de entender las características de rendimiento de Erlang y F#. Encuentro el modelo de concurrencia de Erlang muy atractivo, pero me inclino a usar F # por razones de interoperabilidad. Mientras que fuera de la caja F# no ofrece nada como el de Erlang primitivas de concurrencia from de lo que puedo decir async y MailboxProcessor solo cubren una pequeña porción de lo que Erlang hace bien've He estado tratando de entender lo que es posible en F# rendimiento sabio.

En el libro de programación Erlang de Joe Armstrong, señala que los procesos son muy baratos en Erlang. Él usa (aproximadamente) el siguiente código para demostrar este hecho:

-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)].

En mi Macbook Pro, generar y matar 100 mil procesos (processes:max(100000)) toma alrededor de 8 microsegundos por procesos. Puedo aumentar el número de procesos un poco más, pero un millón parece romper las cosas bastante consistentemente.

Sabiendo muy poco F#, intenté implementar este ejemplo usando async y MailboxProcessor. Mi intento, que puede estar equivocado, es el siguiente:

#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."

Usar F# en Mono, iniciar y matar 100,000 actores/procesadores toma menos de 2 microsegundos por proceso, aproximadamente 4 veces más rápido que Erlang. Lo más importante, quizás, es que puedo escalar a millones de procesos sin problemas aparentes. El inicio de 1 o 2 millones de procesos aún toma alrededor de 2 microsegundos por proceso. Aún es posible iniciar 20 millones de procesadores, pero se ralentiza a unos 6 microsegundos por proceso.

Todavía no me he tomado el tiempo para entender completamente cómo F# implementa async y MailboxProcessor, pero estos resultados son alentadores. Hay algo que estoy haciendo terriblemente mal?

Si no, ¿hay algún lugar donde Erlang probablemente superará a F#? Ser ¿hay alguna razón por la que las primitivas de concurrencia de Erlang no se pueden llevar a F# a través de una biblioteca?

EDITAR: Los números anteriores son incorrectos, debido al error que Brian señaló. Actualizaré toda la pregunta cuando la arregle.

Author: Tristan, 2010-02-07

2 answers

En su código original, solo inició un procesador Mailbox. Haga wait() una función, y llámela con cada yield. Tampoco está esperando a que giren o reciban los mensajes, lo que creo que invalida la información de tiempo; vea mi código a continuación.

Dicho esto, tengo algo de éxito; en mi caja puedo hacer 100,000 a unos 25us cada uno. Después de mucho más, creo que posiblemente empieces a luchar contra el asignador / GC tanto como cualquier otra cosa, pero pude hacer un millón también (a unos 27us cada uno, pero en este punto estaba usando como 1.5 G de memoria).

Básicamente cada 'async suspendido' (que es el estado cuando un buzón está esperando en una línea como

let! msg = inbox.Receive()

) solo toma cierto número de bytes mientras está bloqueado. Es por eso que puede tener mucho, mucho, mucho más asíncosque hilos; un hilo típicamente toma como un megabyte de memoria o más.

Bien, aquí está el código que estoy usando. Puede usar un número pequeño como 10, y define define DEBUG para asegurarse de que la semántica del programa sea lo que es deseado (las salidas de printf pueden estar intercaladas, pero usted tendrá la idea).

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

Todo esto dicho, no conozco a Erlang, y no he pensado profundamente acerca de si hay una manera de recortar el F# más (aunque es bastante idiomático como-es).

 23
Author: Brian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-06 23:48:54

La VM de Erlang no usa subprocesos o procesos del sistema operativo para cambiar al nuevo proceso de Erlang. Su VM simplemente cuenta las llamadas a funciones en su código/proceso y salta al proceso de otra VM después de algunas (en el mismo proceso del sistema operativo y el mismo subproceso del sistema operativo).

CLR utiliza mecánicas basadas en procesos y subprocesos del sistema operativo, por lo que F# tiene un costo general mucho mayor para cada cambio de contexto.

Así que la respuesta a tu pregunta es "No, Erlang es mucho más rápido que los procesos de desove y matanza".

P.d. Usted puede encontrar resultados de ese concurso práctico interesante.

 15
Author: ssp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-01 19:04:41