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:
-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 ~p~n",
[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 ~p~n",
[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:
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)
(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,
wner, :start_time, :total_passes
def initialize(note, passes_remaining = 300)
@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:
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…
Sent 3000 messages in…
WallClock: 79.336 milliseconds
=> nil
The real difference comes out when you increase the numbers:
(Erlang):
<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):
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.