Давно собирался сравнить pros и cons разных «производных» от erlang:spawn(). И вот, собрался.
Сравнения интересно сделать в двух плоскостях: spawn() vs spawn_link() и erlang:spawn() vs proc_lib:spawn().
Как известно, spawn_link() - это тот же spawn(), только вдобавок создает связь новоиспеченного процесса с процессом, его вызвавшим. В этом случае, порожденный процесс при выходе отправит связанному процессу сообщение о выходе.
То же самое можно сделать, если вместо обычного spawn() вызвать что-то в духе:
...
Caller = self(),
spawn(fun() ->
link(Caller),
do_something()
end),
...
Но, если посмотреть на код erlang.erl и net_kernel.erl, то оказывается, что обе функции на самом деле являются производными от spawn_opt(), которая позволяет, помимо автоматического связывания, делать еще некоторые штуки: тюнинг «личного» GC процесса, мониторинг (аналог erlang:monitor(), только, опять же, автоматический) и управление аллоцируемой памятью.
Низкоуровневый код spawn_opt/2 реализован на C, а значит, должен на все лишние действия тратить минимум ресурсов. Так и есть - spawn() и spawn_link() почти неразличимы как по времени выполнения, так и по занимаемой памяти:
1> test:test(erlang, spawn, 1000).
Total execution time: 15073 ms
Total memory occupied by processes: 1345912 bytes
Average memory per process: 1345.912
1> test:test(erlang, spawn, 10000).
Total execution time: 667174 ms
Total memory occupied by processes: 13715136 bytes
Average memory per process: 1371.5136
1> test:test(erlang, spawn_link, 1000).
Total execution time: 15439 ms
Total memory occupied by processes: 1393836 bytes
Average memory per process: 1393.836
1> test:test(erlang, spawn_link, 10000).
Total execution time: 712902 ms
Total memory occupied by processes: 14199572 bytes
Average memory per process: 1419.9572
Следует отметить, что при росте числа процессов, скорость запуска новых процессов заметно уменьшается.
Итого, процесс, порожденный spawn_link(), занимает немного меньше места в памяти и немного медленнее запускается. Но отличие столь ничтожно, что можно смело использовать spawn_link() всегда, по принципу «а вдруг пригодится?».
Немного иначе дело обстоит с модулем proc_lib. Он подается как расово верный OTP-запускатор процессов, который хранит внутри себя дерево процессов-предков, а также дополнительно обрабатывает код завершения {terminate, Something}. Кроме того, в случае падения процесса, sasl передается исчерпывающая информация о произошедшей эпидерсии.
Очевидно, запускаться процесс через proc_lib будет довольно заметно медленнее, а также будет больше весить в памяти.
Сравнение идет сразу со spawn_link(), потому что, как выяснилось выше, разницы почти нет, зато можно отлавливать завершение процесса без лишних телодвижений.
1> test:test(proc_lib, spawn_link, 1000).
Total execution time: 23506 ms
Total memory occupied by processes: 1434604 bytes
Average memory per process: 1434.604
1> test:test(proc_lib, spawn_link, 10000).
Total execution time: 1463338 ms
Total memory occupied by processes: 14202604 bytes
Average memory per process: 1420.2604
Забавно, что с увеличением числа процессов средний размер процесса в памяти в этом случае стабильно уменьшается.
Скорость запуска уменьшилась в полтора-два раза, а вот памяти съедается примерно столько же. Разумеется, оверхед из-за хранения процессов-предков будет расти при увеличении глубины структуры, но, к счастью, хранение как атома, так и pid'а очень дешево.
Какой из этого напрашивается вывод?
В случае, если процессов мало или не нужно очень часто порождать новые - лучше просто использовать proc_lib: больше возможностей для дебага и отлова ошибок через sasl.
Если же процессов десятки или сотни тысяч, и часто приходится запускать новые - есть смысл подумать об использовании spawn'ов из модуля erlang в узких местах.
P.S. Код модуля test, использованного для сравнения:
-module(test).
-export([
loop/1,
test/3
]).
test(Mod, Fun, N) ->
NormalMemory = erlang:memory(processes),
Now = now(),
lists:foreach(fun(I) -> Mod:Fun(test, loop, [I]) end, lists:seq(1, N)), io:format("Total execution time: ~p ms~n", [timer:now_diff(now(), Now)]),
ProcessesMemory = erlang:memory(processes) - NormalMemory,
io:format("Total memory occupied by processes: ~p bytes~n", [ProcessesMemory
]),
io:format("Average memory per process: ~p~n", [ProcessesMemory / N]).
loop(State) ->
loop(State).