Video:

Benchmarking Your Go Code

December 6, 2020

Course Instructor: Elliot Forbes

Hey Gophers! My name is Elliot and I'm the creator of TutorialEdge and I've been working with Go systems for roughly 5 years now.

Twitter: @Elliot_f

One of the main reasons people tend to use Go is for its performance. It’s an incredibly fast compiled and strongly-typed language that is ideal for a large number of use-cases.

For the large majority of use-cases, performance is often “good-enough” within your Go applications and you don’t tend to often need to go down the route of benchmarking your code and analyzing performance to any great detail.

There are however, a number of use cases that require absolute peak performance. It is for these use cases that doing some degree of benchmarking your code is incredibly important. You need to be able to understand the performance characteristics and benchmarking is a fantastic way to consistently do that.

Implementing A Few Simple Benchmarks

Let’s start by creating a new benchmarking test in calculator_test.go. Typically benchmarking tests also reside within your *_test.go files:

package calculator_test

import (
	"testing"

	"github.com/TutorialEdge/go-testing-bible/calculator"
)

...

func BenchmarkCalculateIsArmstrong370(b *testing.B) { 
    for i := 0; i < b.N; i++ {
		calculator.CalculateIsArmstrong(370)
	}
}

Notice how we are following a very similar convention to the Test functions that we have become used to writing. The only differences are that we are replacing the Test prefix with Benchmark and that we have changed the pointer type that we are passing into the function from type testing.T to type testing.B.

Now that we have created our first benchmarking function, we can run these benchmarks in the terminal like so:

$ go test ./... -run=Benchmark -bench=.

You should see that this function will be executed an absolutely huge number of times and you will be given an output with how many seconds it takes per op within your function.

Running Multiple Benchmarks Succinctly

Now, we’ve only benchmarked our function against a single input, if we wanted to extended the number of benchmarks we do with various different inputs, one approach we could take is to create a non-exported benchmark function which takes in an input and then call this within the exported Benchmark functions like so:

package calculator_test

...

func benchmarkCalculateIsArmstrong(input int, b *testing.B) {
	for i := 0; i < b.N; i++ {
		calculator.CalculateIsArmstrong(input)
	}
}

func BenchmarkCalculateIsArmstrong370(b *testing.B) { benchmarkCalculateIsArmstrong(370, b) }
func BenchmarkCalculateIsArmstrong371(b *testing.B) { benchmarkCalculateIsArmstrong(371, b) }
func BenchmarkCalculateIsArmstrong0(b *testing.B)   { benchmarkCalculateIsArmstrong(0, b) }

This will allow us to easily add new benchmarks for this function to our suite of benchmarks with just a single new line!

Running Specific Benchmarks

Some benchmarks can take a fairly significant amount of time to complete depending on the complexity of your code. You may want to specify exactly which benchmarks you want to run for various reasons and thankfully we can use the same -run flag which we use to specify the exact tests that we want to run.

This -run flag will take in a regex pattern that can filter out just the benchmarks that we wish to run and can be useful in situations where you just want to run a lightweight set of benchmarks

Conclusion

Awesome, so in this tutorial, we’ve looked at how you can effectively benchmark your Go applications using the inbuilt benchmarking functionality within the testing package.

We’ll be covering performance analysis in more depth further into the series and looking at how we can generate flame graphs and analyze to a far more specific level where there are bottlenecks in our code.