Действительно ли F # быстрее, чем Эрланг в процессах порождения и убийства? - PullRequest
61 голосов
/ 07 февраля 2010

Обновлено: в этом вопросе содержится ошибка, из-за которой тест не имеет смысла. Я попытаюсь улучшить тест, сравнивая основные функции параллелизма F # и Эрланга, и поинтересуюсь результатами в другом вопросе.

Я пытаюсь понять рабочие характеристики Erlang и F #. Я считаю модель параллелизма Эрланга очень привлекательной, но я склонен использовать F # по соображениям совместимости. В то время как из коробки F # не предлагает ничего похожего на примитивы параллелизма Erlang - из того, что я могу сказать, async и MailboxProcessor покрывают лишь небольшую часть того, что Erlang делает хорошо - я пытался понять, что возможно в производительности F # мудрый.

В книге Джо Армстронга по программированию на Эрланге он подчеркивает, что процессы в Эрланге очень дешевы. Он использует (примерно) следующий код, чтобы продемонстрировать этот факт:

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

На моем Macbook Pro порождение и уничтожение 100 тысяч процессов (processes:max(100000)) занимает около 8 микросекунд на процессы. Я могу поднять количество процессов немного дальше, но миллион, похоже, довольно последовательно ломает вещи.

Зная очень мало F #, я пытался реализовать этот пример, используя async и MailBoxProcessor. Моя попытка, которая может быть ошибочной, заключается в следующем:

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

Использование F # на Mono, запуск и уничтожение 100 000 актеров / процессоров занимает менее 2 микросекунд на процесс, примерно в 4 раза быстрее, чем Erlang. Возможно, более важно то, что я могу масштабировать до миллионов процессов без каких-либо явных проблем. Запуск 1 или 2 миллионов процессов по-прежнему занимает около 2 микросекунд на процесс. Запуск 20 миллионов процессоров все еще возможен, но замедляется примерно до 6 микросекунд на процесс.

Я еще не нашел время, чтобы полностью понять, как F # реализует асинхронный и MailBoxProcessor, но эти результаты обнадеживают. Что-то я делаю ужасно неправильно?

Если нет, есть ли место, где Эрланг, вероятно, превзойдет F #? Есть ли какая-то причина, по которой примитивы параллелизма Эрланга не могут быть доставлены в F # через библиотеку?

РЕДАКТИРОВАТЬ: вышеуказанные цифры неверны, из-за ошибки Брайана указал. Я буду обновлять весь вопрос, когда я это исправлю.

Ответы [ 2 ]

23 голосов
/ 07 февраля 2010

В исходном коде вы запустили только один MailboxProcessor. Сделайте wait() функцией и вызывайте ее с каждым yield. Также вы не ждете, пока они раскрутятся или получат сообщения, которые, я думаю, лишают законной силы информацию о времени; см. мой код ниже.

Тем не менее, у меня есть некоторый успех; на моем ящике я могу сделать 100 000 на 25us каждый. Я думаю, что после слишком большого количества времени вы, возможно, начнете бороться с распределителем / сборщиком мусора так же сильно, как и все остальное, но я смог сделать миллион тоже (примерно по 27 мкс каждый, но на данный момент использовал 1,5 Гб памяти). *

По сути, каждый «приостановленный асинхронный» (то есть состояние, когда почтовый ящик ожидает в строке, такой как

let! msg = inbox.Receive()

) занимает некоторое количество байтов, пока оно заблокировано. Вот почему вы можете иметь гораздо больше асинхронных потоков, чем потоков; поток обычно занимает мегабайт памяти или больше.

Хорошо, вот код, который я использую. Вы можете использовать небольшое число, например 10, и --define DEBUG, чтобы обеспечить желаемую семантику программы (выходные данные printf могут чередоваться, но вы поймете идею).

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

Все это говорит о том, что я не знаю Эрланга, и я не задумывался о том, есть ли способ урезать F # больше (хотя это довольно идиоматично как есть).

15 голосов
/ 07 февраля 2010

Виртуальная машина Erlang не использует потоки или процессы ОС для переключения на новый процесс Erlang. Это виртуальная машина просто считает вызовы функций в вашем коде / процессе и переходит к процессу другой виртуальной машины после некоторого (в тот же процесс ОС и тот же поток ОС).

CLR использует механизмы, основанные на процессах и потоках ОС, поэтому F # имеет гораздо более высокие накладные расходы для каждого переключения контекста.

Итак, ответ на ваш вопрос: «Нет, Эрланг намного быстрее, чем процессы нереста и убийств».

P.S. Вы можете найти результаты этого практического конкурса интересные.

...