author
Kevin Kelche

Golang Benchmarking: The Complete Guide


Introduction

In Go, Benchmarking is a method of evaluating a program’s performance by measuring how long it takes to execute and how much memory it consumes under various conditions. It can assist you in identifying areas of your code that can be optimized and comparing different implementations to determine which is the most effective.

Note: Benchmarking results can vary from machine to machine and from run to run. Some benchmarks may be more sensitive to the environment than others.

How to Write a Benchmark

Writing benchmarks involve using the testing package. To write a benchmark to a function, in a fileName_test.go file just like any other test. Next, declare a function with a Benchmark prefix. The function takes a pointer to testing.B as its only argument. testing.B has a N field that is the number of times the benchmark should be run. The function should run the code to be benchmarked b.N times.

By using the testing.B’s N field, the benchmark will run the code to be benchmarked b.N times. The benchmark will then report the average time it took to run the code to be benchmarked. The benchmark will also report the number of times the code to be benchmarked was run per second.

example.go
package main

import "fmt"

func myFunction(){
  for i := 0; i < 1000000; i++ {
    fmt.Println(i)
  }
}

Copied!

example_test.go
package main

import "testing"

func BenchmarkMyFunction(b *testing.B) {
  for i := 0; i < b.N; i++ {
    myFunction()
  }
}

Copied!

How to Run a Benchmark

To run a benchmark, use the go test command. The -bench flag is used to specify the benchmark to run. The -benchmem flag is used to report the memory allocations made by the benchmark. The -benchtime flag is used to specify the amount of time to run the benchmark. The -count flag is used to specify the number of times to run the benchmark.

Command
go test -bench=. -benchmem -benchtime=10s -count=5

Copied!

Let’s look at the output of the above command.

Output
goos: linux
goarch: amd64
pkg: github.com/kelcheone/benchmarking
cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
BenchmarkMyFunction-12       1000000        1000 ns/op       0 B/op         0 allocs/op
PASS
ok    github.com/kelcheone/benchmarking  10.000s

Copied!

The first line of the output is the operating system and architecture of the machine the benchmark was run on. The second line is the package that was benchmarked. The third line is the CPU that was used to run the benchmark. The fourth line is the name of the benchmark and the number of CPUs that were used to run the benchmark. The fifth line is the number of times the benchmark was run, the average time it took to run the benchmark, the average number of bytes allocated per operation, and the average number of allocations per operation. The sixth line is the result of the benchmark. The seventh line is the package that was benchmarked and the amount of time it took to run the benchmark.

How to run Benchmarks only

By default, the go test command will run all tests and benchmarks in the package. To run only benchmarks, use the -run flag and set it to ^$. This will run no tests.

Command
go test -bench=. -run=^$

Copied!

How to Write a Benchmark Table

Benchmark tables are a way to compare the performance of different implementations of the same function. To create a benchmark table, use the testing.B’s Run method. The Run method takes a string and a function as arguments. The string is the name of the benchmark. The function is the code to be benchmarked. The function should take a pointer to a testing.B as its only argument. The testing.B has a N field that is the number of times the benchmark should be run. The function should run the code to be benchmarked b.N times.

example.go
package main

import "fmt"

type myStruct struct {
  name string
  age int
}

func (m myStruct) randomFunction(){
  fmt.Println(m.name)
}

Copied!

example_test.go
package main

import (
  "testing"
)

func BenchmarkMyFunction(b *testing.B) {
  m := []myStruct{
    {name: "Kevin", age: 20},
    {name: "Jane", age: 21},
    {name: "John", age: 22},
  }
  b.Run("Method", func(b *testing.B) {
    for i := 0; i < b.N; i++ {
      m[0].randomFunction()
      m[1].randomFunction()
      m[2].randomFunction()
    }
  })
  b.Run("Function", func(b *testing.B) {
    for i := 0; i < b.N; i++ {
      randomFunction(m[0])
      randomFunction(m[1])
      randomFunction(m[2])
    }
  })

}

func randomFunction(m myStruct) {
  // println(m.name)
}

Copied!

In the above example, we are comparing the performance of calling a method on a struct vs calling a function with a struct as an argument. The Run method is used to create two benchmarks. The first benchmark is named Method and the second benchmark is named Function. The Method benchmark calls the randomFunction method on the struct. The Function benchmark calls the randomFunction function with the struct as an argument.

How to Benchmark a Function with Arguments

Below is an example of how to benchmark a function with arguments.

example.go
package main

import "fmt"

func myFunction(name string, age int){
  fmt.Println(name, age)
}

Copied!

example_test.go
package main

import "testing"

func BenchmarkMyFunction(b *testing.B) {
  for i := 0; i < b.N; i++ {
    myFunction("Kevin", 20)
  }
}

Copied!

How to Benchmark an API Call

Below is an example of how to benchmark an API call.

example.go
package main

import (
  "fmt"
  "net/http"
)

func myFunction(){
  resp, err := http.Get("https://example.com")
  if err != nil {
    fmt.Println(err)
  }
  defer resp.Body.Close()
}

Copied!

example_test.go
package main

import "testing"

func BenchmarkMyFunction(b *testing.B) {
  for i := 0; i < b.N; i++ {
    myFunction()
  }
}

Copied!

In this example, we are going to benchmark the performance of a function that does a linear search on a slice of integers. The function will return the index of the first occurrence of the integer in the slice. If the integer is not in the slice, the function will return -1.

example.go
package main

import "fmt"

func linearSearch(arr []int, target int) int {
  for i, v := range arr {
    if v == target {
      return i
    }
  }
}

Copied!

example_test.go
package main

import (
  "testing"
)

func BenchmarkLinearSearch(b *testing.B) {
  arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  for i := 0; i < b.N; i++ {
    linearSearch(arr, 5)
  }
}

Copied!

The output of the benchmark is:

Output
goos: linux
goarch: amd64
pkg: github.com/kelcheone/example
BenchmarkLinearSearch-4         76395093                16.94 ns/op
PASS
ok      github.com/kelcheone/example  1.889s

Copied!

In this example, we are going to benchmark the performance of a function that does a binary search on a slice of integers. The function will return the index of the first occurrence of the integer in the slice. If the integer is not in the slice, the function will return -1.

example.go
package main

import "fmt"

func binarySearch(arr []int, target int) int {
  low := 0
  high := len(arr) - 1
  for low <= high {
    mid := (low + high) / 2
    if arr[mid] == target {
      return mid
    } else if arr[mid] < target {
      low = mid + 1
    } else {
      high = mid - 1
    }
  }
  return -1
}

Copied!

example_test.go
package main

import (
  "testing"
)

func BenchmarkBinarySearch(b *testing.B) {
  arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  for i := 0; i < b.N; i++ {
    binarySearch(arr, 5)
  }
}

Copied!

The output of the benchmark is:

Output
goos: linux
goarch: amd64
pkg: github.com/kelcheone/example
BenchmarkBinarySearch-4         418724697                2.470 ns/op
PASS
ok      github.com/kelcheone/example  3.111s

Copied!

In Example 1, the linear search function took 16.94 ns/op to run. In Example 2, the binary search function took 2.470 ns/op to run. The binary search function is 6.8 times faster than the linear search function. Even though the binary search function is faster, it is more complex to implement. The binary search function is a good example of the tradeoff between performance and complexity.

Conclusion

We discussed techniques for benchmarking functions in Go, such as benchmarking a method on a struct or benchmarking a function with arguments, in this article. We also investigated how to benchmark an API call, as well as the performance of a linear search function versus a binary search function, while keeping the tradeoffs between performance and complexity in mind.

Subscribe to my newsletter

Get the latest posts delivered right to your inbox.