Contents

How to use Go Benchmarking


This article covers how to use Go’s built in benchmarking tools


Why?

If you’ve ever wondered about how much time removing that second “for loop” in your function saves or the access speed diference between a dynamic array and a link list array then you might be interested in Go’s built in benchmarking tools.

Example fuctions:

I have two functions that complete the same task, these are taken from the following Leetcode question: single-number

The first is my solution using a map to track the values as to only iterate over them once: Try yourself

package main

// singleNumber: Given an array of numbers, return the number that occurs only once.
func singleNumber(nums []int) int {
	m := make(map[int]int)

	for i, _ := range nums {
		if _, ok := m[nums[i]]; ok {
			delete(m, nums[i])
		} else {
			m[nums[i]] = nums[i]
		}
	}
	for k := range m {
		return k
	}
	return 0
}

func main() {
    print(singleNumber([]int{4, 1, 2, 1, 2}))
}

The second is someone else’s answer which is much simpler and I suspect much faster: Try Yourself

package main

func singleNumberBitwise(nums []int) (res int) {

	for _, v := range nums {
		res ^= v
	}

	return res
}

func main(){
	print(singleNumberBitwise([]int{4, 1, 2, 1, 2}))
}

Writing benchmark test

The benchmark tool comes from the standard library Testing, the benchmarking functions commonly live in the same file as your test code and should start with Benchmark.

Here is an example for our two functions above:

package main
import "testing"

func Benchmark_singleNumber(b *testing.B) {
	for i := 0; i < b.N; i++ {
		singleNumber([]int{4, 1, 2, 1, 2})
	}
}

func Benchmark_singleNumberBitwise(b *testing.B) {
	for i := 0; i < b.N; i++ {
		singleNumberBitwise([]int{4, 1, 2, 1, 2})
	}
}

Every benchmarking function is going to contain a “for loop” to stop the benchmark once b.N is satisfied. During benchmark execution, b.N is adjusted until the benchmark function lasts long enough to be timed reliably.

Running benchmark tests

Now that we have the benchmark functions we can run both of them with go test -bench=.:

❯ go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: Golang/go_benchmark_test
Benchmark_singleNumber-10                8217672               136.5 ns/op            48 B/op          2 allocs/op
Benchmark_singleNumberBitwise-10        423836629                2.830 ns/op           0 B/op          0 allocs/op
PASS
ok      Golang/go_benchmark_test        2.868s

From this output we can see the beachmark provided us the following understanding:

  • It ran with 10 CPU cores: Benchmark_singleNumber-10, Benchmark_singleNumberBitwise-10
  • The bitwise function ran 423836629 times vs 8217672
  • The bitwise function only took 2.830 nanoseconds vs 136.5 nanoseconds per loop!
  • The bitwise function used 0 allocations per loop vs 2 at 48 bytes.

Summary

The fact that the bitwise function doesn’t need to make any allocations is why it’s much faster than the map alternative

Optional arguments

  • go test -bench=N N takes in regex, when we input . we want it to run everything, but you can filter what benchmarks are ran.
  • go test -bench=. -count=N N takes number of times to run the benchmark test, this can be useful as anything happening on your system when benchmarks are ran can impact the results.
  • go test -bench=. -count=N -benchmem -benchmem enables memory allocation statistics

REF: https://pkg.go.dev/cmd/go#hdr-Testing_flags