2010-02-06 12 views
60

Actualizado: Esta pregunta contiene un error que hace que el punto de referencia no tenga sentido. Intentaré un mejor punto de referencia comparando la funcionalidad de concurrencia básica de F # y Erlang e investigaré los resultados en otra pregunta.¿Es F # realmente más rápido que Erlang en los procesos de desove y muerte?

Estoy tratando de entender las características de rendimiento de Erlang y F #. Encuentro el modelo de simultaneidad de Erlang muy atractivo, pero me inclino a usar F # por razones de interoperabilidad. Mientras que F # no ofrece nada como las primitivas de simultaneidad de Erlang, de lo que puedo decir como asincronía y MailboxProcessor solo cubre una pequeña porción de lo que Erlang hace bien, he estado tratando de entender qué es posible en el rendimiento de F #. sabio.

En el libro Programación Erlang de Joe Armstrong, señala que los procesos son muy baratos en Erlang. Él usa la (más o menos) 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, el desove y matando a 100 mil procesos (processes:max(100000)) tarda unos 8 microsegundos por procesos. Puedo aumentar el número de procesos un poco más, pero un millón parece romper las cosas de forma bastante consistente.

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

usuario de F # en Mono, con salida y matando a 100.000 actores/procesadores toma menos de 2 microsegundos por proceso, aproximadamente 4 veces más rápido que Erlang. Más importante aún, quizás, es que puedo escalar hasta millones de procesos sin ningún problema aparente. Comenzar 1 o 2 millones de procesos todavía toma aproximadamente 2 microsegundos por proceso. El inicio de 20 millones de procesadores sigue siendo factible, pero se desacelera a aproximadamente 6 microsegundos por proceso.

Todavía no me he tomado el tiempo para comprender 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 supere F #? ¿Hay alguna razón por la que las primitivas de simultaneidad de Erlang no puedan ser llevadas a F # a través de una biblioteca?

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

+1

+1 para una pregunta verdaderamente avanzada. – gahooa

+0

+1 para ser interesante también y no solo avanzado. – gradbot

+1

has usado la opción "erl + native" para Erlang? (ver http://stackoverflow.com/questions/2207451/erlang-compilation-mixed-of-hipe-object-code-and-opcode) – jldupont

Respuesta

23

En su código original, solo inició un MailboxProcessor. Haga wait() una función, y llámelo con cada yield. Además, no estás esperando que se muevan o reciban los mensajes, lo que creo que invalida la información de sincronización; mira mi código a continuación.

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

Básicamente, cada 'asíncrono suspendida' (que es el estado cuando un buzón está esperando en una línea como

let! msg = inbox.Receive() 

) sólo toma un número de bytes mientras que está bloqueado. Es por eso que puedes tener mucho, mucho, mucho más asyncs que hilos; un hilo generalmente toma como megabytes de memoria o más.

Ok, aquí está el código que estoy usando. Puede usar un número pequeño como 10 y --definir DEBUG para asegurarse de que la semántica del programa sea la deseada (las salidas de printf pueden estar entrelazadas, pero se entenderá).

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 

dicho todo esto, no sé Erlang, y no he pensado profundamente acerca de si hay una manera de recortar el F # más (aunque es bastante idiomática como está).

+0

uh oh. Sospeché que podría estar cometiendo algún tipo de error como este. – Tristan

+0

Solo para confirmar, debería reemplazar tanto wait with wait()? Obtuve resultados mucho peores como este, en el rango de 500 microsegundos. No he digerido lo que realmente está sucediendo, por lo que su segundo punto es probablemente un problema aún mayor. – Tristan

+0

El código anterior me da aproximadamente 1000 microsegundos por proceso, que es 100 veces peor que Erlang. Pero no estoy seguro de que estos dos ejemplos sean iguales. La disminución de una variable mutable parece muy poco Erlang. Entiendo que los actores solo deben hablar a través de mensajes. – Tristan

15

La máquina virtual de Erlang no utiliza procesos o subprocesos del sistema operativo para cambiar al nuevo proceso de Erlang. Su VM simplemente cuenta las llamadas de función en su código/proceso y salta al proceso de otra VM después de algunas (en el mismo proceso de sistema operativo y la misma cadena de sistema operativo).

CLR utiliza mecanismos basados ​​en el proceso del sistema operativo y los hilos, por lo que F # tiene un costo adicional mucho mayor para cada cambio de contexto.

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

P.S. Puede encontrar results of that practical contest interesante.

+0

Eso es lo que pensé, y es por eso que encuentro mis resultados confusos. Pero estaban equivocados. Sin embargo, ¿es esta una limitación inherente o simplemente la forma en que las cosas están diseñadas actualmente? No lo he visto del todo, pero aquí se describe una sola sincronización asíncrona: http://cs.hubfs.net/blogs/hell_is_other_languages/archive/2008/08/03/6506.aspx – Tristan

+0

Es un buen ejemplo de los hilos del espacio de usuario. Puedes ver las fuentes de F #, su asincronización usa CLR ThreadPool. – ssp

+8

F # asyncs son hilos de espacio de usuario. No tienen la sobrecarga asociada con los hilos del sistema operativo. Pueden emplear el grupo de subprocesos para explotar múltiples núcleos, pero nunca hay más subprocesos de los que el hardware admite de forma nativa, y los tipos comunes de cambio de contexto permanecen dentro de una única cadena de sistema operativo. Con un solo núcleo (no hyperthreading) son hilos de espacio puramente de usuario como Erlang. – RD1

Cuestiones relacionadas