【Golang】benchmark

Posted by 西维蜀黍 on 2021-07-13, Last Modified on 2021-09-21

The Golang testing package contains a benchmarking facility that can be used to examine the performance of your Golang code. In this article we’ll see how to write simple benchmark tests that are able to provide us good insights about a given algorithmic solution.

The good old Fibonacci number calculation

Recursive approach

When you look at the Fibonacci algorithm, it seems to be very straightforward to implement in nearly any programming language. And probably the first approach to solve it is to use recursion:

package fibo

func RecursiveFibonacci(n uint) uint {
	if n <= 1 {
		return n
	}
	return RecursiveFibonacci(n-1) + RecursiveFibonacci(n-2)

}

Each iteration in the series discards the previous results and then re-calculates the intermediate steps for each subsequent iteration.

Let’s add some unit tests:

package fibo

import "testing"

func TestRecursiveFibonacci(t *testing.T) {
	data := []struct {
		n    uint
		want uint
	}{
		{0, 0},
		{1, 1},
		{2, 1},
		{3, 2},
		{4, 3},
		{5, 5},
		{6, 8},
		{10, 55},
		{42, 267914296},
	}
	for _, d := range data {
		if got := RecursiveFibonacci(d.n); got != d.want {
			t.Errorf("got: %d, want: %d", got, d.want)
		}
	}
}

It works:

$ go test -run TestRecursiveFibonacci
PASS
ok      awesomeProject/fibo     1.680s

Sequential approach

This alternative implementation removes the recursion and instead uses a simple for loop and a couple of variables. If you think about it, the algorithm is nothing but a sum of N numbers. We start from 0 and 1 and we will start adding subsequent sums:

package fibo

func SequentialFibonacci(n uint) uint {
    if n <= 1 {
        return uint(n)
    }
    var n2, n1 uint = 0, 1
    for i := uint(2); i < n; i++ {
        n2, n1 = n1, n1+n2
    }
    return n2 + n1
}

Let’s add some unit tests:

func TestSequentialFibonacci(t *testing.T) {
    data := []struct {
        n    uint
        want uint
    }{
        {0, 0},
        {1, 1},
        {2, 1},
        {3, 2},
        {4, 3},
        {5, 5},
        {6, 8},
        {10, 55},
        {42, 267914296},
    }
    for _, d := range data {
        if got := SequentialFibonacci(d.n); got != d.want {
            t.Errorf("got: %d, want: %d", got, d.want)
        }
    }
}

It also works:

$ go test -run TestSequentialFibonacci                     
PASS
ok      awesomeProject/fibo     0.274s

Notice that we’ve got a considerable performance improvement here: 1.680s versus 0.274s.

Benchmarking

In order to measure performance, we could measure execution time and display it with some print statements, of course. But Golang offers a very sophisticated tooling for benchmarking, and it’s fairly simple to use.

Writing a benchmark is very similar to writing a test as they share the infrastructure from the testing package. Some of the key differences are:

  • Benchmark functions start with ‘Benchmark’, not ‘Test’;
  • Benchmark functions are run several times by the testing package. The value of ‘b.N’ will increase each time until the benchmark runner is satisfied with the stability of the benchmark;
  • Each benchmark must execute the code under test b.N times. Thus, a ‘for’ loop will be present in every benchmark function.

Our final fibo_test.go file will contain both unit and benchmark tests:

package fibo

import "testing"

func BenchmarkTestRecursiveFibonacci_10(b *testing.B) {
	for i := 0; i < b.N; i++ {
		RecursiveFibonacci(10)
	}
}
func BenchmarkTestRecursiveFibonacci_20(b *testing.B) {
	for i := 0; i < b.N; i++ {
		RecursiveFibonacci(20)
	}
}
func BenchmarkTestSequentialFibonacci_10(b *testing.B) {
	for i := 0; i < b.N; i++ {
		SequentialFibonacci(10)
	}
}
func BenchmarkTestSequentialFibonacci_20(b *testing.B) {
	for i := 0; i < b.N; i++ {
		SequentialFibonacci(20)
	}
}

With benchmark tests in place, all we need to do is to invoke it via “go test -bench=.”. By default, it runs using all the CPUs available.

$ go test -bench=.
goos: darwin
goarch: amd64
pkg: awesomeProject/fibo
BenchmarkTestRecursiveFibonacci_10-32            4308085               242 ns/op
BenchmarkTestRecursiveFibonacci_20-32              40873             29340 ns/op
BenchmarkTestSequentialFibonacci_10-32          352406025                3.47 ns/op
BenchmarkTestSequentialFibonacci_20-32          214937937                5.56 ns/op
PASS
ok      awesomeProject/fibo     7.683s

Ouput format above is

Benchmark<test-name>-<number-of-cpus> <number of executions> <duration of each operation>

Reference