Erlang & Ruby -- Ring Network
As part of my Erlang self-education, I’m doing a selection of sample problems (from the “Programming Erlang” book) in both Erlang and my language of choice: Ruby. The idea is to explore the differences between these very different languages.
Today’s problem is to write a ring network benchmark. Given a network of N nodes, where each node is pointing to the next in line (or back to the first), each node should forward a received message along, until it has gone around the ring M times (so that a total of N * M messages are sent).
To start with, I came up with an Erlang module to spawn a set of processes, and pass along a message record. It was pretty ugly at first, but with a little refactoring it turned out alright:
-module(
ring_network).
-export(
[
start_ring
/
1,
start_message
/
3
]).
-record(
message,
{
loops_remaining
=
300,
times_sent
=
0,
runtime,
wall_clock,
owner,
note
}).
start_ring(
N)
->
spawn(
fun()
->
FirstPid
= start_ring(
N
-
1,
self()),
io:format(
“Starting node
~p
(
~p
) pointing to
pn
“,
[
N,
self(),
FirstPid
]),
loop(
N,
FirstPid)
end).
start_ring(
N,
Next)
->
Pid
= start_node(
N,
Next),
if
N
>
1
-> start_ring(
N
-
1,
Pid);
true
->
Pid
end.
start_node(
Id,
Next)
->
spawn(
fun()
->
io:format(
“Starting node
~p
(
~p
) pointing to
pn
“,
[
Id,
self(),
Next
]),
loop(
Id,
Next)
end).
start_message(
Pid,
Msg,
N)
->
{
Runtime,
_
}
=
statistics(
runtime),
{
WallClock,
_
}
=
statistics(
wall_clock),
Pid
!
#message
{
loops_remaining
=
N,
runtime
=
Runtime,
wall_clock
=
WallClock,
owner
=
Pid,
note
=
Msg
}.
loop(
Id,
Next)
->
receive
Message
->
case
Message
#message.
owner
==
self()
of
false
->
Next
!
Message
#message
{
times_sent
=
Message
#message.
times_sent
+
1
},
loop(
Id,
Next);
true
->
case
Message
#message.
loops_remaining
==
0
of
false
->
Next
!
Message
#message
{
times_sent
=
Message
#message.
times_sent
+
1,
loops_remaining
=
Message
#message.
loops_remaining
-
1
},
loop(
Id,
Next);
true
->
print_totals(
Message),
loop(
Id,
Next)
end
end
end.
print_totals(
Message)
->
{
R2,
_
}
=
statistics(
runtime),
{
W2,
_
}
=
statistics(
wall_clock),
U1
= (
R2
-
Message
#message.
runtime),
U2
= (
W2
-
Message
#message.
wall_clock),
io:format(
"
~n
“),
io:format(
“Sent '
~p
'
~p
times in..
~n
“,
[
Message
#message.
note,
Message
#message.
times_sent
]),
io:format(
" Runtime:
~p
milliseconds
~n
“,
[
U1
]),
io:format(
" WallClock:
~p
milliseconds
~n
“,
[
U2
]).
Next I applied the same pattern in ruby. I created a Node class, told it where the next node was, and had it forward messages along the proper number of times:
class
Node
attr_accessor
:next_node,
:number
def
initialize(number, next_node =
nil)
@number = number
@next_node = next_node
end
def
self.
ring_network(node_count)
nodes =
Array.new(node_count)
nodes[
0] =
Node.new(
(
1..node_count-
1).each
do |
i|
nodes[i] =
Node.new(i+
1, nodes[i-
1])
end
nodes[
0].next_node = nodes[-
1]
nodes[-
1]
end
def
gets_message(msg)
if msg.owner ==
self.object_id
if (msg.passes_remaining-=
1) ==
0
msg.print_totals
return
end
end
msg.owner =
self.object_id
if msg.owner.nil?
msg.total_passes +=
1
next_node.gets_message(msg)
end
end
class
Message
attr_accessor
:runtime,
:wallclock,
:note,
:passes_remaining,
:owner,
:start_time,
:total_passes
def
initialize(note, passes_remaining =
@total_passes =
0
@start_time =
Time.now
@note = note
@passes_remaining = passes_remaining
end
def
print_totals
puts
puts
"
Sent
#{total_passes
}
messages in…
"
puts
"
WallClock:
#{(
Time.now -
@start_time) *
1000
}
milliseconds
"
end
end
The first impression is just what you’d expect: Ruby is easier to read, and erlang is *much* faster:
1
>
Pid
=
ring_network:start_ring(
10).
Starting
node
10 (
<
0.59.
0
>)
pointing
to
<
0.68.
0
>
Starting
node
9 (
<
0.60.
0
>)
pointing
to
<
0.59.
0
>
Starting
node
8 (
<
0.61.
0
>)
pointing
to
<
0.60.
0
>
Starting
node
7 (
<
0.62.
0
>)
pointing
to
<
0.61.
0
>
Starting
node
6 (
<
0.63.
0
>)
pointing
to
<
0.62.
0
>
Starting
node
5 (
<
0.64.
0
>)
pointing
to
<
0.63.
0
>
Starting
node
4 (
<
0.65.
0
>)
pointing
to
<
0.64.
0
>
Starting
node
3 (
<
0.66.
0
>)
pointing
to
<
0.65.
0
>
Starting
node
2 (
<
0.67.
0
>)
pointing
to
<
0.66.
0
>
Starting
node
1 (
<
0.68.
0
>)
pointing
to
<
0.67.
0
>
<
0.59.
0
>
2
>
ring_network:start_message(
Pid,
hello_world,
300).
{
message,
300,
0,
270,
298496,
<
0.59.
0
>,
hello_world
}
Sent
‘hello_world’
3000
times
in..
Runtime:
0
milliseconds
WallClock:
3
milliseconds
Versus…
irb(main):
001:
0> node =
Node.ring_network(
10); msg =
Message.new(
'
Hi!
‘,
300); node.gets_message(msg)
Sent
3000 messages
in…
WallClock:
79.336 milliseconds
=>
nil
The real difference comes out when you increase the numbers:
(Erlang):
1
>
Pid
=
ring_network:start_ring(
10000).
<
0.33.
0
>
2
>
ring_network:start_message(
Pid,
hello_world,
10000).
{
message,
10000,
0,
460,
40488,
<
0.33.
0
>,
hello_world
}
Sent
‘hello_world’
100000000
times
in..
Runtime:
95350
milliseconds
WallClock:
157455
milliseconds
(Ruby):
irb(main):
002:
0> node =
Node.ring_network(
100); msg =
Message.new(
'
Hi!
‘,
300); node.gets_message(msg)
SystemStackError: stack level too deep
from ./ring_network.rb:
29
:in
'
gets_message
’
from ./ring_network.rb:
29
:in
'
gets_message
’
from (irb):
2
from :
0
In both cases, we’re using recursion to wait for another message after processing one. In Erlang’s case, though, it compiles tail recursion as a JUMP statement, which doesn’t eat up stack space. Ruby doesn’t do this, which is why I get a “stack too deep” when I up the number of loops. To get around this in ruby, I would have to move towards the Erlang module of forking off separate processes (not just separate instances), each listening for some interprocess communication.