Ist F # wirklich schneller als Erlang bei Laichen und töten Prozesse?
-
19-09-2019 - |
Frage
Aktualisiert: Diese Frage enthält einen Fehler, der die Benchmark bedeutungslos macht. Ich werde eine bessere Benchmark-Vergleich F # und Erlang Grund Gleichzeitigkeit Funktionalität versuchen und über die Ergebnisse in einer anderen Frage erkundigen.
Ich versuche, die Leistungseigenschaften von Erlang und F # verstehen. Ich finde Erlang Concurrency-Modell sehr ansprechend, aber ich geneigt Fis aus Gründen der Interoperabilität zu verwenden. Während aus der Box F # bietet nichts wie Primitive Erlang Gleichzeitigkeit - von dem, was ich sagen kann, async und MailboxProcessor deckt nur einen kleinen Teil von dem, was Erlang tut gut - ich habe zu verstehen versucht, was in F # Leistung möglich ist, weise.
In Joe Armstrong Programming Erlang Buch, er macht den Punkt, dass Prozesse in Erlang sehr billig sind. Er nutzt den (grob) den folgenden Code ein, diese Tatsache zu demonstrieren:
-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)].
Auf meinem MacBook Pro, Laich- und tötet 100.000 Prozesse (processes:max(100000)
) dauert etwa 8 Mikrosekunden pro Prozesse. Ich kann die Anzahl der Prozesse ein bisschen weiter erhöhen, aber eine Million scheint die Dinge ziemlich konsequent zu brechen.
sehr wenig F # Wissen, habe ich versucht, diesem Beispiel zu implementieren async und MailBoxProcessor verwenden. Mein Versuch, die falsch sein kann, ist wie folgt:
#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."
Mit F # auf Mono ab und tötet 100.000 Schauspieler / Prozessoren nehmen unter 2 Mikrosekunden pro Prozess, etwa 4-mal schneller als Erlang. Noch wichtiger ist vielleicht, dass ich ohne offensichtliche Probleme für Millionen von Prozessen skalieren. Ab 1 oder 2 Millionen Prozesse dauert noch etwa 2 Mikrosekunden pro Prozess. Ab 20 Millionen Prozessoren ist immer noch möglich, aber verlangsamt bis etwa 6 Mikrosekunden pro Prozess.
Ich habe noch nicht die Zeit genommen, um vollständig zu verstehen, wie F # async und MailBoxProcessor implementiert, aber diese Ergebnisse sind ermutigend. Gibt es etwas, das ich schrecklich falsch?
Wenn nicht, gibt es einen Ort Erlang wird wahrscheinlich Fis übertreffen? Gibt es einen Grund Primitiven Erlang Gleichzeitigkeit kann nicht auf F # über eine Bibliothek gebracht werden?
EDIT: Die oben genannten Zahlen sind falsch, aufgrund des Fehlers Brian hingewiesen. Ich werde die ganze Frage aktualisieren, wenn ich es beheben.
Lösung
In Ihrem ursprünglichen Code, Sie begann nur einen MailboxProcessor. Machen wait()
eine Funktion, und rufen Sie es mit jedem yield
. Auch sind Sie nicht auf sie warten, die Nachrichten spinnen oder empfangen, die ich denke, die Timing-Informationen ungültig; siehe meinen Code unten.
Das heißt, ich habe einen gewissen Erfolg; auf meiner Box kann ich 100.000 auf etwa 25US tun jeder. Nach zu viel mehr, ich glaube, vielleicht starten Sie das Allocator / GC so viel wie etwas zu kämpfen, aber ich war in der Lage eine Million zu tun (bei 27us über jeden, aber an diesem Punkt wurde wie 1.5G Speicher verwendet wird).
Grundsätzlich jeder ‚suspendiert async‘ (was der Zustand ist, wenn ein Postfach auf einer Linie wartet wie
let! msg = inbox.Receive()
) hat nur eine bestimmte Anzahl von Bytes, während es gesperrt ist. Deshalb sollten Sie Art und Weise haben, viel, viel mehr als asyncs Fäden; ein Thread erfolgt typischerweise wie ein Megabyte Speicher oder mehr.
Ok, hier ist der Code, den ich verwenden. Sie können eine kleine Zahl wie 10 verwenden, und --define DEBUG das Programm Semantik ist, um sicherzustellen, was gewünscht wird (printf Ausgänge verschachtelt sein können, aber Sie bekommen die Idee).
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
Alle sagte dies, ich weiß nicht, Erlang, und ich habe nicht tief darüber nachgedacht, ob eine Art und Weise gibt es die F # mehr zu trimmen (obwohl es ziemlich idiomatische ist, wie sie ist).
Andere Tipps
Erlang VM nicht verwendet nicht OS-Threads oder Prozess auf neuen Erlang Prozess zu wechseln. Es ist VM zählt einfach Funktionsaufrufe in Ihren Code / Prozess und springt auf andere VMs Prozess nach einigen (in gleichen OS-Prozess und gleichen OS-Thread).
CLR verwendet Mechanik basiert auf OS-Prozess und Threads, so F # viel höhere Gemeinkosten für jeden Kontextschalter hat.
So Antwort auf Ihre Frage ist: „Nein, Erlang ist viel schneller als Laich- und töten Prozesse“.
P. S. Sie können finden Ergebnisse dieser praktischen Wettbewerb interessant.