61 votes

F# est-il vraiment plus rapide qu'Erlang pour faire naître et tuer des processus ?

Mis à jour : Cette question contient une erreur qui rend le benchmark sans intérêt. Je vais essayer de faire un meilleur benchmark en comparant les fonctionnalités de base de F# et d'Erlang en matière de concurrence et demander les résultats dans une autre question.

J'essaie de comprendre les caractéristiques de performance d'Erlang et de F#. Je trouve le modèle de concurrence d'Erlang très attrayant, mais je suis enclin à utiliser F# pour des raisons d'interopérabilité. Alors que F# n'offre rien de comparable aux primitives de concurrence d'Erlang - de ce que je peux dire, async et MailboxProcessor ne couvrent qu'une petite partie de ce qu'Erlang fait bien - j'ai essayé de comprendre ce qui est possible dans F# en termes de performances.

Dans le livre Programming Erlang de Joe Armstrong, il fait remarquer que les processus sont très bon marché en Erlang. Il utilise le code suivant (en gros) pour démontrer ce fait :

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

Sur mon Macbook Pro, la création et la destruction de 100 000 processus ( processes:max(100000) ) prend environ 8 microsecondes par processus. Je peux augmenter le nombre de processus un peu plus, mais un million semble casser les choses de façon assez constante.

Connaissant très peu F#, j'ai essayé d'implémenter cet exemple en utilisant async et MailBoxProcessor. Ma tentative, qui peut être erronée, est la suivante :

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

En utilisant F# sur Mono, le démarrage et l'arrêt de 100 000 acteurs/processeurs prennent moins de 2 microsecondes par processus, soit environ 4 fois plus vite qu'en Erlang. Le plus important, peut-être, est que je peux passer à des millions de processus sans aucun problème apparent. Lancer 1 ou 2 millions de processus prend toujours environ 2 microsecondes par processus. Le démarrage de 20 millions de processeurs est toujours possible, mais ralentit à environ 6 microsecondes par processus.

Je n'ai pas encore pris le temps de bien comprendre comment F# met en œuvre l'asynchronisme et MailBoxProcessor, mais ces résultats sont encourageants. Y a-t-il quelque chose que je fais terriblement mal ?

Sinon, y a-t-il un endroit où Erlang sera probablement plus performant que F# ? Y a-t-il une raison pour que les primitives de concurrence d'Erlang ne puissent pas être apportées à F# par le biais d'une bibliothèque ?

EDIT : Les chiffres ci-dessus sont faux, en raison de l'erreur que Brian a signalée. Je mettrai à jour la question entière quand je l'aurai corrigée.

23voto

Brian Points 82719

Dans votre code original, vous n'avez démarré qu'un seul MailboxProcessor. Faites wait() une fonction, et l'appeler avec chaque yield . De plus, vous n'attendez pas qu'ils tournent ou qu'ils reçoivent les messages, ce qui, à mon avis, invalide l'information sur le timing ; voir mon code ci-dessous.

Cela dit, j'ai un peu de succès ; sur ma machine, je peux en faire 100 000 à environ 25us chacun. Après un peu plus, je pense que vous commencez à vous battre contre l'allocateur/GC, mais j'ai pu en faire un million aussi (à environ 27us chacun, mais à ce stade, j'utilisais environ 1,5G de mémoire).

Fondamentalement, chaque "asynchrone suspendu" (qui est l'état dans lequel une boîte aux lettres est en attente sur une ligne du type

let! msg = inbox.Receive()

) ne prend qu'un certain nombre d'octets pendant qu'il est bloqué. C'est pourquoi vous pouvez avoir beaucoup, beaucoup, beaucoup plus d'asynchrones que de threads ; un thread prend typiquement un mégaoctet de mémoire ou plus.

Ok, voici le code que j'utilise. Vous pouvez utiliser un petit nombre comme 10, et --define DEBUG pour vous assurer que la sémantique du programme est celle que vous souhaitez (les sorties printf peuvent être entrelacées, mais vous aurez l'idée).

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

Cela dit, je ne connais pas Erlang et je n'ai pas réfléchi à la possibilité de réduire davantage le F# (bien qu'il soit assez idiomatique tel quel).

15voto

ssp Points 1384

La VM d'Erlang n'utilise pas les threads ou processus du système d'exploitation pour passer à un nouveau processus Erlang. La VM compte simplement les appels de fonction dans votre code/processus et saute vers le processus d'une autre VM après certains (dans le même processus OS et le même thread OS).

Le CLR utilise une mécanique basée sur les processus et les threads du système d'exploitation, de sorte que F# a un coût de surcharge beaucoup plus élevé pour chaque changement de contexte.

La réponse à votre question est donc "Non, Erlang est beaucoup plus rapide que de faire naître et tuer des processus".

P.S. Vous pouvez trouver les résultats de ce concours pratique intéressant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X