Languages


Erlang vs Go: Ring Messages

Topic: Languages
Date: 31/10/2016

This post is about exercise 3 in chapter 12 of the Programming Erlang - Software for a Concurrent World book. The premise of the exercise is to create a ring of concurrent processes which each send a message around the ring. The message should be passed $M$ times around a ring of $N$ processes. The completion time is then measured for the $M \cdot N$ messages passed and plotted on graphs to compare complexity and efficiency between the languages.

So, for the exercise I decided that Go would be a suitable competitor to Erlang as they can both create processes/goroutines easily and cheaply. Unfortunately, as I am more experienced with Go, the results might be in favour for Go. In order to try and mitigate any discrepancies, I am going to try and create very similar ways of passing the message around the ring.

Erlang Ring

For the Erlang ring, as I am not sure how to create structures (If you can at all) the best thing I could do is to spawn all the processes and store their Pid's in a list. When first creating the ring, I would access the required ring element by using the lists:nth function along with an index passed along in the message. This was soon changed to having a spent ring process list and the upcoming process list. Then when one process in the ring has been sent a message, it is moved to the spent list. This increased the performance of the ring operations significantly as it is $O(1)$ rather than $O(n).$ In the most recent version of the code, the ring has been constructed so that each process controls the Pid for the next process in the ring by passing it to the ring_element function. With the new changes in the erlang code, the differences between the code are minimal and so should be a more accurate test. The code for the erlang ring looks somewhat like the following.

% create_ring will return the data structure which is used
% for all of the ring functions. This is essentially a list of
% Pid's for processes which have already been spawned and are
% waiting for a message from the previous process in the ring.
% 
% 	N - the number of processes in the ring
% 	
create_ring(N) -> 
	Pid = spawn(ring, ring_start, []),
	create_ring(N, [Pid], Pid).
create_ring(1, [PPid|Acc], Start) -> 
	Start ! {PPid},
	Ring = [PPid|Acc],
	lists:reverse(Ring);
create_ring(N, [PPid|Acc], Start) -> 
	Pid = spawn(ring, ring_element, [PPid]),
	NewAcc = [Pid|[PPid|Acc]],
	create_ring(N-1, NewAcc, Start).

% ring_element will handle the passing of a message to the 
% next process in the ring. This happens until the message
% reaches the end of the ring.
% 
% 	NPid - this is the Pid of the next ring member
% 	
ring_element(NPid) -> 
	receive 
		Payload ->
			NPid ! Payload,
			ring_element(NPid)
	end.

The table below shows the time taken (s) for a message to be sent $M$ times around a ring of $N$ processes.

Processes Number of times message sent around the ring ($M$)
0 200 400 600 800 1000
100 0 0.019014 0.037506 0.054008 0.066509 0.09101
1000 0 0.188024 0.376047 0.529625 0.744093 0.893612
10000 0 1.755221 3.562469 5.322169 7.069391 8.983079

erlang graph for 1000 processes

Go Ring

The Go ring is fairly similar to the Erlang ring except that it uses structures to hold the next Node as a pointer. The communication between the nodes is via a channel, which is the closest method to Erlang's send primitive. However, the Go code has been optimised somewhat in that only the start of the ring has to check when the message has gone around the ring, which saves a lot of computation. Thus the Go code will be a fair bit faster. The most important part of this blog is to just show the linear relation between creating more processes and the time taken to complete the message sending. The following is a snippet of the Go code.

// StartNodes takes a ring of nodes and starts the processes. It also
// creates a channel for the ring to send back information on when it
// is completed. This function will block until the ring has finished
func (r *Ring) StartNodes() {
	reply := make(chan bool)
	go r.Start.HandleStartNode(r.MessageNum, reply)
	for current := r.Start.Next; current != r.Start; current = current.Next {
		go current.HandleNode()
	}
	r.Start.In <- r.Message
	<-reply
}

// HandleNode will receive and send any messages in the ring
// this is how the ring works. When the ring should no longer send
// a message the channel is closed.
func (n *Node) HandleNode() {
	for message := range n.In {
		n.Next.In <- message
	}
}
Processes Time taken for $M$ processes (s)
0 200 400 600 800 1000
100 0 0.0080012 0.018002 0.0265028 0.0385034 0.0445047
1000 0 0.0760249 0.1530192 0.2320464 0.3050381 0.389548
10000 0 0.7300922 1.528035 2.3277921 3.1648963 3.9280171

go graph for 1000 processes

Screenshots:

Exponential graph for quick data analysis 10 process Erlang ring 100 process Erlang ring 1000 process Erlang ring 10 process Go ring 100 process Go ring 1000 process Go ring

Links:

Ring Data | Go Ring | Erlang Ring