Go vs Crystal Performance

Pavel Timofeev · June 10, 2020

Go vs Crystal

You can join the discussion on HackerNews here.

It’s a follow up post to the previous Ruby vs Crystal Performance.

I guess this time it will be a fair performance comparison as both languages are compiled and statically typed.

We will perform a couple of tests:

  • Finding a number in the Fibonacci sequence as in the previous post
  • Running an HTTP server locally and performing benchmarks with wrk

Language versions installed my machine are:

  • go version go1.14.3 darwin/amd64
  • Crystal 0.34.0 (2020-04-07)

I’m curious to find out how Go and Crystal perform in comparison to each other.

Compilation

For the tests we will be running previously compiled programs. We will use the release flag to enable optimizations in Crystal:

crystal build --release program.cr

Go binaries don’t have a release version and we won’t be using any flags. So, it’s just:

go build program.go

Fibonacci

Alright, first we will write code to generate a Fibonacci sequence for a given number. Let’s find the 47th number which is 2,971,215,073.

Go version:

package main

import "fmt"

func fibonacci(n uint32) uint32 {
  if n < 2 {
    return n
  }
  return fibonacci(n-1) + fibonacci(n-2)
}

func main() {
  fmt.Println(fibonacci(47))
}

Crystal version:

def fibonacci(n : UInt32)
  return n if n < 2
  fibonacci(n-1) + fibonacci(n-2)
  end

puts fibonacci(47)

Results on my machine (MacBook Pro 2.2 GHz Intel Core i7):

Language Binary size Run time Memory usage
go 2.1M 21.28s 2.01M
Crystal 418k 19.69s 1.72M

Crystal is slightly winning here.

A few observations here:

Crystal’s binary size is 5 times smaller than Go’s. Though, they can be slightly reduced in size when we omit the debug information:

go build -ldflags="-w" fibonacci_golang.go

This way the binary size goes down from 2.1M to 1.7M.

Also, not in this particular example, but generally Go’s compilation time is much much faster than Crystal’s.

HTTP Server

Now, let’s create a simple HTTP server using standard libraries. Both Go’s net/http and Crystal’s http/server employ concurrency: Go uses goroutines and Crystal uses fibers.

Go version:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", HelloServer)
	http.ListenAndServe(":8080", nil)
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello from %s!", r.URL.Path[1:])
}

Crystal version:

require "http/server"

server = HTTP::Server.new do |context|
  context.response.content_type = "text/plain"
  context.response.print "Hello from #{context.request.path}!"
end

puts "Listening on http://127.0.0.1:8080"
server.listen(8080)

For benchmarking we will be using wrk. If you’re not familiar with this tool it’s like a pretty well known ApacheBench (ab) but a modern version.

Here is how we can run a benchmark for 60 seconds, using 8 threads, and keeping 400 HTTP connections open:

wrk -t8 -c400 -d60s http://localhost:8080/hello

Results for the Go server:

Running 1m test @ http://localhost:8080/hello
  8 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.56ms    2.26ms  95.31ms   92.00%
    Req/Sec     8.77k     2.24k   15.75k    64.66%
  4190457 requests in 1.00m, 535.51MB read
  Socket errors: connect 157, read 100, write 0, timeout 0
Requests/sec:  69757.81
Transfer/sec:      8.91MB

Results for the Crystal server:

Running 1m test @ http://localhost:8080/hello
  8 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.89ms    0.97ms  19.01ms   80.34%
    Req/Sec    10.54k     3.41k   18.14k    60.85%
  5035284 requests in 1.00m, 513.82MB read
  Socket errors: connect 157, read 85, write 0, timeout 0
Requests/sec:  83917.26
Transfer/sec:      8.56MB

Results:

Language Binary size Memory usage CPU usage Throughput
go 7.4M 20.2M 300% 69,757
Crystal 966kb 19.1M 99% 83,917

Crystal again shows better results.

CPU utilization over 100% in the table might seem confusing. But it simply means the system uses multiple cores. One core at max is 100%.

My machine has 8 cores as it can be seen with the following command on macOs:

sysctl -n hw.ncpu

Conclusion

Frankly speaking, we have only performed a couple of small tests to make any conclusions but I’m still excited for Crystal as a young language but showing great results.

Twitter, Facebook