Czy F # jest naprawdę szybszy od Erlanga w procesie tarła i zabijania?

Aktualizacja: to pytanie zawiera błąd, który sprawia, że benchmark nie ma znaczenia. Spróbuję lepszego benchmarka porównującego podstawową funkcjonalność współbieżności F# i Erlanga i zapytam o wyniki w innym pytaniu.

Staram się zrozumieć charakterystykę Erlanga i F#. Uważam, że model współbieżności Erlanga jest bardzo atrakcyjny, ale jestem skłonny używać F# ze względów interoperacyjności. Podczas gdy po wyjęciu z pudełka F # nie oferuje niczego podobnego do Erlanga współbieżność - z tego co mogę powiedzieć async i MailboxProcessor obejmują tylko niewielką część tego co Erlang robi dobrze - próbowałem zrozumieć co jest możliwe w F# performance wise. W książce Joe Armstrong "Programowanie Erlanga" zwraca uwagę, że procesy w Erlangu są bardzo tanie. Używa (z grubsza) następującego kodu, aby zademonstrować ten fakt:
-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)].

Na moim MacBooku Pro zrobienie i zabicie 100 tysięcy procesów (processes:max(100000)) zajmuje około 8 mikrosekund na procesy. Mogę podnieść liczbę procesów nieco dalej, ale milion wydaje się łamać rzeczy dość konsekwentnie.

Znając bardzo mało F#, próbowałem zaimplementować ten przykład używając async i MailBoxProcessor. Moja próba, która może być błędna, wygląda następująco:

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

Używanie F# na Mono, uruchomienie i zabicie 100 000 aktorów/procesorów zajmuje mniej niż 2 mikrosekundy na proces, około 4 razy szybciej niż Erlang. Co ważniejsze, być może, mogę zwiększyć skalę do milionów procesów bez widocznych problemów. Uruchomienie 1 lub 2 milionów procesów trwa nadal około 2 mikrosekund na proces. Uruchomienie 20 milionów procesorów jest nadal możliwe, ale spowalnia do około 6 mikrosekund na proces.

Nie poświęciłem jeszcze czasu, aby w pełni zrozumieć, jak F# implementuje async i procesor MailBoxProcessor, ale te wyniki są zachęcające. Czy jest coś, co robię strasznie źle?

Jeśli nie, to czy jest jakieś miejsce, gdzie Erlang prawdopodobnie przewyższy F#? Na jest jakiś powód, dla którego Erlang nie może być przeniesiony do F# przez Bibliotekę?

EDIT: powyższe liczby są błędne, z powodu błędu, na który wskazywał Brian. Zaktualizuję całe pytanie, kiedy to naprawię.

Author: Tristan, 2010-02-07

2 answers

W oryginalnym kodzie uruchomiłeś tylko jeden procesor skrzynki pocztowej. Stwórz wait() funkcję i wywołaj ją z każdym yield. Również nie czekasz na nich, aby zakręcić lub otrzymać wiadomości, które myślę, że unieważnia informacje o czasie; Zobacz mój kod poniżej.

To powiedziawszy, mam pewien sukces; na moim pudełku mogę zrobić 100,000 po około 25us każdy. Po za dużo więcej, myślę, że można zacząć walczyć alokator / GC jak wszystko, ale byłem w stanie zrobić milion zbyt (na około 27us każdy, ale w tym momencie używałem jak 1,5 g pamięci).

W zasadzie każdy ' suspended async '( czyli stan, w którym Skrzynka pocztowa czeka na linii jak

let! msg = inbox.Receive()

) zajmuje tylko pewną liczbę bajtów, gdy jest zablokowany. Dlatego możesz mieć znacznie więcej asynchronicznych niż wątków; wątek zazwyczaj zajmuje megabajt pamięci lub więcej.

Ok, oto kod, którego używam. Możesz użyć małej liczby, takiej jak 10, i --define DEBUG, aby upewnić się, że semantyka programu jest tym, co jest pożądane (wyjścia printf mogą być przeplatane, ale dostaniesz pomysł).
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
[4]] to wszystko powiedziane, Nie wiem Erlang, i nie myślałem głęboko o tym, czy jest sposób, aby przyciąć F #już (choć jest to dość idiomatyczne jak-jest).
 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-02-06 23:48:54

VM Erlanga nie używa wątków OS ani procesu, aby przełączyć się na nowy proces Erlanga. To VM po prostu zlicza wywołania funkcji do Twojego kodu / procesu i przeskakuje do innego procesu VM po niektórych (do tego samego procesu OS i tego samego wątku OS).

CLR wykorzystuje mechanikę opartą na procesie OS i wątkach, więc F # ma znacznie wyższe koszty ogólne dla każdego przełącznika kontekstowego.

Więc odpowiedź na twoje pytanie brzmi: "nie, Erlang jest znacznie szybszy niż tarło i procesy zabijania".

P. S. możesz znaleźć wyniki praktycznego konkursu ciekawe.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-01 19:04:41