Ring Benchmark - my first concurrent Erlang

Following the advice of Dave Thomas who said: “any developer should learn a new language every year” on RailsConf 2007 in Berlin, I decided that Erlang was going to be my challenge for 2008. However, the first months of 2008 passed without any Erlang until I listened to Joe Armstrong himself at Qcon in London. His talk was inspiring to me so right afterwards I bought his book and start reading it on my flight home.

After working myself through the first chapters, I finally got to the part that explained concurrent programming. In his book, he invites the reader to create an Erlang program that implements the Ring Benchmark. So I did and here is my version.

Problem

Create N processes in a ring. Send a message round the ring M times so that a total of N * M messages get sent. Time how long this takes for different values of N and M.

Solution

-module(ring_benchmark).
-export([start/2]).
%
% Create a ring of N processes through which a message is send M times.
% First, create a chain of processes: First,...,Last
% Then send a composed message to the first process containing:
% - throw, the type of request
% - the message (payload)
% - the process id of the last created process of the ring
% - number of times the message must be thrown through the ring.
%
% @author Ernest Micklei, PhilemonWorks.com, May 2008
%
start(N,M) -> 
    First = spawn(fun() -> receive_send(void) end),
    io:format("Created ~p~n",[First]),
    Last = connect(First,N-1),
    io:format("~p will send messages to ~p~n", [First,Last]),
    First ! {throw,ball,Last,M},
    done.
%
% Create a new process that will send messages to the process with ID = To
% Repeat this Count times, forming a chain (not a ring!) of processes. 
% Return the process ID of the last created process.
%
connect(To,Count) ->
    Next = spawn(fun() -> receive_send(To) end),
    io:format("Created ~p~n",[Next]),
    io:format("~p will send messages to ~p~n", [Next,To]),
    case (Count =:= 1) of
        true -> Next;
        false -> connect(Next,Count-1)
    end.
%
% Each process will exectue this in a loop.
% To is the process ID of the next process in the ring.
% If a throw request is received then send it to the ring.
% When the message is received, throw it again if the count > 0 
% Else, send a die request to kill all processes of the ring.
%
receive_send(To) ->
    receive 
        {Message,Ring,Count} -> 
            case (To =:= void) of           
                true ->
                    io:format("~p received ~p (~p)~n",[self(),Message,Count]),
                    case (Count =:= 0) of
                        true -> 
                            {_,TimeAfter} = statistics(runtime),
                            io:format("Completed in ~p [ms]~n",[TimeAfter]),
                            Ring ! die;
                        false ->
                            Ring ! {Message,Ring,Count-1}
                    end,
                    receive_send(void);
                false ->
                    io:format("~p received ~p (~p), sending it to: ~p~n",
                        [self(),Message,Count,To]), 
                    To ! {Message,Ring,Count},
                    receive_send(To)
            end;
        {throw,Message,Ring,Count} ->       
            io:format("start throwing ~p ~p times through ring starting at: ~p~n",
                [Message,Count,Ring]), 
            statistics(runtime),
            Ring ! {Message,Ring,Count},
            receive_send(To);
        die ->
            io:format("~p died~n",[self()]),
            case (To =:= void) of
                true -> void;
                false -> To ! die
            end
    end.

Download source

If you are interested in numbers, N=10.000, M=10.000 on my Mac Book Pro took 101.290 milliseconds (runtime). I might update this post later after doing some more tests with different parameter sets.

comments powered by Disqus