Comparing Ruby, C, and Go

puzzle for comparing Ruby, C, and Go

What do we learn when we solve the same problem in Ruby, C, and Go? How might the solutions differ in flexibility, readability, and performance?

The Hashrocket team presented a snake_case programming challenge during Ancient City Ruby last week. Nineteen attendees submitted correct solutions. Three of the solvers were selected at random to receive a prize: Raspberry Pi 3.

One of the solvers, Jack Christensen of Hashrocket, gave a lightning talk about his approach. The contest called for a solution in Ruby. Jack added two more languages: C and Go.

The Challenge

Here’s the challenge, as presented to the Ancient City Ruby audience:

You’ve just arrived in sunny St. Augustine, and find yourself amazed by the visionary civic planning that would result in the area in which you now stand: a street grid exactly 10 blocks square.
 
You’re in the northwest corner of this 10 by 10 block area, and would like to take a scenic walk to the southeast corner, while only ever moving south or east.
 
As you begin walking, you wonder to yourself, “how many different paths could I take from this northwest corner to the southeast corner?”
 
You quickly note that if the downtown area were only a 2 block by 2 block grid, there would be 6 distinct paths from one corner to the other (see the diagram at the top of this post).
 
So, how many distinct paths are there through the 10 by 10 downtown area?

We’ll assume that the standard compass points apply to the map: North is at the top.

Studying the Problem

Jack observed a few things about the scenic route:

Any solution that nets 10 south plus 10 east will get the traveler to the end of the route.

Data Representation

“Two choices… sounds like binary to me”, Jack observed during his lightning talk. So he decided to represent each journey as a 20-bit binary number:

A successful journey will consist of exactly ten “1” bits and ten “0” bits. The 1s and 0s can be in any order.

This similar to a coin-flipping problem. The flipper flips a coin twenty times, recording heads or tails after each flip. How many different ways can we flip the coin so that the end result produces exactly ten heads and exactly ten tails?

Start Small (2x2)

The binary model works with the small, 2x2 block example at the beginning of this post.

0000
0001
0010
0011 (solution)
0100
0101 (solution)
0110 (solution)
0111
1000
1001 (solution)
1010 (solution)
1011
1100 (solution)
1101
1110
1111

There are six solutions represented in binary, the same number of solutions shown graphically in 2x2 block example.

Math Solution for 10x10

A mathematician, observing that we want to choose 10 bits from a fixed set of 20 bits, might use combinatorics:

Ruby, C, and Go comparison calculations with combinatorics

Any correct algorithm will give us the same result: 184,756 paths that fit the constraints of the challenge.

Brute Force Solution in Ruby

A brute force solution will solve the problem by looking at every possible combination of bits, one by one. Easy for a small dataset like the 2x2 example. But for larger datasets, a brute force algorithm will consume more time.

Of course, brute force can be fun! Here’s Jack’s brute force solution in Ruby.


puts (0..(2**20)).count { |n| n.to_s(2).chars.count("1") == 10 }

This one-line program…

Brute force!

We can use OS X’s time command to measure performance.


$ time ruby main.rb
184756

real	0m3.129s
user	0m3.115s
sys	0m0.012s

$ 

Ruby found the correct result, 184756, in just over three seconds.

Updated Ruby Version (14Apr2016)

In the comments below, rbhander observed that we can write the brute force Ruby solution without the chars method. It works, and it saves time:

puts (0..(2**20)).count { |n| n.to_s(2).count("1") == 10 }

Before we run the new Ruby version, let’s use the chmod command to make it executable.

$ ls -al main-nochars.rb
-rw-r--r--  1 rth  staff  59 Apr 12 00:22 main-nochars.rb

$ chmod +x main-nochars.rb

$ ls -al main-nochars.rb
-rwxr-xr-x  1 rth  staff  59 Apr 12 00:22 main-nochars.rb

$ 

Timing the faster Ruby version…

$ time ruby ./main-nochars.rb
184756

real	0m0.567s
user	0m0.555s
sys	0m0.010s

$ 

The version without chars executes in 567ms, much faster than the first Ruby version. Open source rocks when we share ideas like this!

Brute Force in C

We expect C to be faster because it’s compiled and closer to the hardware. Let’s see if that’s true. Here’s Jack’s solution in C:

#include <stdio.h>
#include <stdint.h>

#define SQUARE_SIZE 10

int main() {
  int64_t paths = 0;
  for(int64_t i = 0; i < (int64_t) (1)<<(SQUARE_SIZE*2); i++) {
    int64_t bitCount = __builtin_popcount(i);
    if(bitCount == SQUARE_SIZE) {
      paths++;
    }
  }

  printf("%lld\n", paths);
  return 0;
}

How long does it take to find the solution in C? First, let’s compile the program…

$ gcc main.c -o main

$ 

…and run it using time to measure performance.

$ time ./main
184756

real	0m0.009s
user	0m0.006s
sys	0m0.002s

$ 

Execution in nine milliseconds. Running close to the metal has its advantages! The C solution offers other benefits:

Brute Force in Go

And now for the brute force Go solution.

package main

import (
	"fmt"

	"github.com/cznic/mathutil"
)

const squareSize = 10

func main() {
	var paths int64
	for i := uint64(0); i < uint64(1<<(squareSize*2)); i++ {
		bitCount := mathutil.PopCountUint64(i)
		if bitCount == squareSize {
			paths++
		}
	}

	fmt.Println(paths)
}

Before we compile and run the program, we need to grab a math library from github.com/cznic/mathutil. Here’s how to do that.


$ go get github.com/cznic/mathutil

$ 

Next, we compile the program. Note that Go is particular about directory structure. Unlike with C, the source file and the executable file reside in different directories by default, even with a small Go program. Therefore, the next few illustrations will include the full path of the directories where the commands were executed on my machine.

~/Code/gocode/src/github.com/rayhightower/acr16$ go install

~/Code/gocode/src/github.com/rayhightower/acr16$ 

Next, switch to the directory where the new executable is located, and run it while using time to measure performance.


~/Code/gocode/bin$ time ./acr16
184756

real	0m0.015s
user	0m0.009s
sys	0m0.004s

~/Code/gocode/bin$ 

Same result, 184756, as expected. Execution in fifteen milliseconds. Much faster than Ruby, but not as fast as C.

Like C, this Go solution offers some advantages:

Observations

Programming challenges help to keep the mind flexible. Flexible brains are most helpful for problem solving.

C and Go, as compiled languages, will always execute faster than Ruby. Ruby, as an interpreted language, will always make it easy for developers to iterate and experiment because there’s no compile step to break our rhythm.

Gratitude

Special thanks to Ancient City Ruby for sharing a challenge that stretches the brain, and to Jack Christensen for solving the problem three ways.

WindyCityThings - IoT conference in Chicago, IL, USA. Windy City Things.

WindyCityThings: IoT in June 2016

Are you exploring the Internet of Things? Then you might like the WindyCityThings IoT conference. Get the maximum return for the time you invest. Move forward on your next IoT project with confidence. Tickets are on sale now.

Comments