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.
- You can change like this:
go test -cpu=4 -bench=.
. - More flags: https://pkg.go.dev/cmd/go#hdr-Testing_flags
$ 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
- https://pkg.go.dev/testing
- https://hackernoon.com/how-to-write-benchmarks-in-golang-like-an-expert-0w1834gs