Crash Course on Golang Benchmarks: A Beginner's Perspective

  • YC YC
  • |
  • 29 January 2024
post-thumb

Golang, renowned for its simplicity and efficiency, employs benchmark testing as a fundamental tool for performance evaluation. In this exploration, our focus is on the basics of Golang benchmarking, offering a concise understanding of how developers utilize this built-in testing feature to assess and optimize code performance. Let’s delve into the fundamental aspects that shape Golang benchmark tests and their significance in ensuring code efficiency.

Writing a benchmark test

To understand the basics of Golang benchmarking, let’s start by writing a benchmark test for a function that generates a list of prime numbers up to N.

import (
	"math"
)

func GeneratePrimes(N int) []int {
	var primes []int
	for num := 2; num <= N; num++ {
		if IsPrime(num) {
			primes = append(primes, num)
		}
	}
	return primes
}

func IsPrime(num int) bool {
	if num < 2 {
		return false
	}
	// Check for divisibility up to the square root of num
	sqrtNum := int(math.Sqrt(float64(num)))
	for i := 2; i <= sqrtNum; i++ {
		if num%i == 0 {
			return false
		}
	}
	return true
}

Benchmark test will be written in the _test.go file. We write the following benchmark test

import (
	"testing"
)

func BenchmarkGeneratePrimes100(b *testing.B) {
	for n := 0; n < b.N; n++ {
		GeneratePrimes(100)
	}
}

This will run the GeneratePrimes(100) function b.N times where it will generate a list of prime numbers up to 100.

Running a benchmark test

To execute the benchmark test, use the following command:

go test -benchmem -bench=. -run=^$ ./...

This command runs all benchmark tests in the current package and its subpackages, including memory statistics.

The memory statistic of the heap during benchmark test will be included with the usage of -benchmem parameter . The -run=^$ parameter allows only test prefix with “Benchmark” to run, therefore excludes all other tests.

You will see the following output

cpu: 12th Gen Intel(R) Core(TM) i7-1255U
BenchmarkGeneratePrimes100-12            1272073               916.4 ns/op           504 B/op          6 allocs/op
PASS

We can create more benchmark test function with varying inputs and observe the difference in behavior. This can be easily done by creating a helper function:

func benchmarkPrimes(i int, b *testing.B) {
	for n := 0; n < b.N; n++ {
		GeneratePrimes(i)
	}
}

func BenchmarkGeneratePrimes100(b *testing.B) {
	benchmarkPrimes(100, b)
}

func BenchmarkGeneratePrimes1000(b *testing.B) {
	benchmarkPrimes(1000, b)
}

func BenchmarkGeneratePrimes10000(b *testing.B) {
	benchmarkPrimes(10000, b)
}

Let’s run the benchmark tests:

cpu: 12th Gen Intel(R) Core(TM) i7-1255U
BenchmarkGeneratePrimes100-12            1272073               916.4 ns/op           504 B/op          6 allocs/op
BenchmarkGeneratePrimes1000-12             60162             18135 ns/op            4088 B/op          9 allocs/op
BenchmarkGeneratePrimes10000-12             2900            421787 ns/op           25208 B/op         12 allocs/op
PASS

Explaining the result output of a benchmark

You will see 5 distinct columns in the result output. Let’s now break down and explain every column.

ColumnExampleDescription
1BenchmarkGeneratePrimes100-12The name of the benchmark test. This consist of the function name and suffix with a -XX where XX represents your GOMAXPROC.
21272073Represents b.N which is the number of iterations the GeneratePrimes() function has ran.
3916.4 ns/opThe average amount of time taken to execute a single iteration.
4504 B/opThe average number of bytes allocated to the heap per iteration.
56 allocs/opThe average number of heap allocation per iteration.

Avoid compiler optimization

Sometimes compiler may attempt to optimize the code. In our example you can see that the return value of GeneratePrimes() function is never used and there are no side effect within the function. Hence compiler may decides to optimize the application by removing the function entirely, since it does not affect the output of the program. This results in run time of the benchmark being artificially reduced.

To prevent compiler optimization during benchmarking, modify the benchmark tests to ensure all function outputs are used and stored. For example:

var primeList []int

func benchmarkPrimes(i int, b *testing.B) {
	var generatedPrime []int

	for n := 0; n < b.N; n++ {
		generatedPrime = GeneratePrimes(i)
	}

	primeList = generatedPrime
}

Conclusion

Golang offers a straightforward yet powerful mechanism for conducting benchmarks on your functions. Leveraging the built-in testing package, developers can seamlessly assess the performance of their code, gaining valuable insights into execution times and resource utilization.

comments powered by Disqus

You May Also Like