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 vs8217672
- The bitwise function only took
2.830
nanoseconds vs136.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