← recent

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
    

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

== 

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:

milliseconds
  

WallClock:

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.