Aug 242009
 

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 ~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:

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)
    (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 = 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:

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.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>