Golang Benchmark and Testing [Step-by-Step]

Golang Benchmarking Code

Benchmarking measures the performance of a function or program, allowing you to compare implementations and to understand the performance impact of code changes. Using that information, you can easily reveal the part of the code that needs to be rewritten to improve its performance.

Go follows certain conventions regarding benchmarking. The most important convention is that the name of a benchmark function must begin with Benchmark. After the Benchmark word, we can put an underscore or an uppercase letter. Therefore, both BenchmarkFunctionName() and Benchmark_functionName() are valid benchmark functions whereas Benchmarkfunctionname() is not. The same rule applies to testing functions that begin with Test. Although we are allowed to put the testing and benchmarking code on the same file with the other code, it should be avoided. By convention such functions are put in files that end with _test.go. Once the benchmarking or the testing code is correct, the go test subcommand does all the dirty work for you, which includes scanning all *_test.go files for special functions, generating a proper temporary main package, calling these special functions, getting the results, and generating the final output.

Advertisement

Starting from Go 1.17, we can shuffle the execution order of both tests and benchmarks with the help of the shuffle parameter (go test -shuffle=on). The shuffle parameter accepts a value, which is the seed for the random number generator, and can be useful when you want to replay an execution order. Its default value is off. The logic behind that capability is that sometimes the order in which tests and benchmarks are executed affects their results.

There are a number of flags that are available during the invocation of the benchmarking utility. A few helpful flags for benchmarking can be found below:

  • -benchtime t : Run enough iterations of the test to take the defined t duration. Increasing this value will run more iterations of b.N.
  • -count n : Run each test n times.
  • -benchmem : Turn on memory profiling for your test.
  • -cpu x,y,z : Specify a list of GOMAXPROCS values for which the benchmarks should be executed.

 

Setup Lab Environment

To prepare for golang benchmarking, let us write a simple go code. We will create a directory tests and create all our codes inside this directory.

Add a file named main.go to the tests folder with the content shown below:

package main

import (
	"fmt"
	"sort"
)

func sortAndTotal(vals []int) (sorted []int, total int) {
	sorted = make([]int, len(vals))
	copy(sorted, vals)
	sort.Ints(sorted)
	for _, val := range sorted {
		total += val
		total++
	}
	return
}
func main() {
	nums := []int{100, 20, 1, 7, 84}
	sorted, total := sortAndTotal(nums)
	fmt.Println("Sorted Data:", sorted)
	fmt.Println("Total:", total)
}

The sortAndTotal function contains a deliberate error that will help demonstrate the testing features in the next section.

$ go run main.go 
Sorted Data: [1 7 20 84 100]
Total: 212

 

Golang Unit Testing

Create Unit Test Cases

Unit tests are defined in files whose name ends with _test.go. To create a simple test, add a file named simple_test.go to the tests folder with the content shown below:

Advertisement
package main

import "testing"

func TestSum(t *testing.T) {
	testValues := []int{10, 20, 30}
	_, sum := sortAndTotal(testValues)
	expected := 60
	if sum != expected {
		t.Fatalf("Expected %v, Got %v", expected, sum)
	}
}

The Go standard library provides support for writing unit tests through the testing package. Unit tests are expressed as functions whose name starts with Test, followed by a term that begins with an uppercase letter, such as TestSum. (The uppercase letter is important because the test tools will not recognize a function name such as Testsum as a unit test.)

 

Running Unit Tests

The test in the previous code calls the sumAndTotal function with a set of values and compared the result to the expected outcome using a standard Go comparison operator. If the result isn’t equal to the expected value, then the Fatalf method is called, which reports the test failure and stops any remaining statements in the unit test from being executed (although there are no remaining statements in this example).

# go test
--- FAIL: TestSum (0.00s)
    simple_test.go:10: Expected 60, Got 63
FAIL
exit status 1
FAIL    _/opt/deepak/scripts/goexamples/tests   0.040s

The output from the tests reports the error as well as the overall outcome of the test run. Let's fix our code

	for _, val := range sorted {
		total += val
		// total++
	}
	return

Save the change and run the go test command, and the output will show that the test passes:

# go test
PASS
ok      _/opt/deepak/scripts/goexamples/tests   0.033s

A test file can contain multiple tests, which will be discovered and executed automatically. Below we have added a second test function to the simple_test.go file.

package main

import (
	"sort"
	"testing"
)

func TestSum(t *testing.T) {
	testValues := []int{10, 20, 30}
	_, sum := sortAndTotal(testValues)
	expected := 60
	if sum != expected {
		t.Fatalf("Expected %v, Got %v", expected, sum)
	}
}
func TestSort(t *testing.T) {
	testValues := []int{1, 279, 48, 12, 3}
	sorted, _ := sortAndTotal(testValues)
	if !sort.IntsAreSorted(sorted) {
		t.Fatalf("Unsorted data %v", sorted)
	}
}

The TestSort test verifies that the sortAndTotal function sorts data. Notice that I can rely on the features provided by the Go standard library in unit tests and use the sort.IntsAreSorted function to perform the test. Run the go test command, and you will see the following outcome:

# go test
PASS
ok      _/opt/deepak/scripts/goexamples/tests   0.042s

The go test command doesn’t report any detail by default, but more information can be generated by using -v as shown below:

# go test -v
=== RUN   TestSum
--- PASS: TestSum (0.00s)
=== RUN   TestSort
--- PASS: TestSort (0.00s)
PASS
ok      _/opt/deepak/scripts/goexamples/tests   0.046s

 

Golang Benchmark Testing

Create benchmarking code

Functions whose name started with Benchmark, followed by a term that begins with an uppercase letter, such as Sort, are benchmarks, whose execution is timed. Add a file named benchmark_test.go to the tests folder with the content shown below:

package main

import (
	"math/rand"
	"testing"
	"time"
)

func BenchmarkSort(b *testing.B) {
	rand.Seed(time.Now().UnixNano())
	size := 250
	data := make([]int, size)
	for i := 0; i < b.N; i++ {
		for j := 0; j < size; j++ {
			data[j] = rand.Int()
		}
		sortAndTotal(data)
	}
}

 

Performing benchmarks

The BenchmarkSort function creates a slice with random data and passes it to the sortAndTotal function. Run the benchmark test:

# go test -bench . -run notest
goos: linux
goarch: amd64
cpu: Intel Core Processor (Haswell, no TSX, IBRS)
BenchmarkSort-4            26616             44259 ns/op
PASS
ok      _/opt/deepak/scripts/goexamples/tests   1.773s

The period following the -bench argument causes all of the benchmarks that the go test tool discovers to be performed. The period can be replaced with a regular expression to select specific benchmarks. By default, the unit tests are also performed, but since I introduced a deliberate error into the TestSum function, I used the -run argument to specify a value that won’t match any of the test function names in the project, with the result that only the benchmarks will be performed.

The following is an example of benchmark execution. In our example execution, we are profiling our existing TestSum benchmark twice. We're also using four GOMAXPROCS, viewing the memory profiling for our test, and performing these requests for 2 seconds instead of the default 1-second test invocation. We can invoke our go test -bench functionality like this:

# go test -bench=. -benchtime 2s -count 2 -benchmem -cpu 4 -run notest
goos: linux
goarch: amd64
cpu: Intel Core Processor (Haswell, no TSX, IBRS)
BenchmarkSort-4           101664             23184 ns/op            2072 B/op          2 allocs/op
BenchmarkSort-4           106664             22301 ns/op            2072 B/op          2 allocs/op
PASS
ok      _/opt/deepak/scripts/goexamples/tests 22.304s

A benchmark will run until the function returns, fails, or skips. The results of the benchmark are returned as a standard error once the test has completed. After the tests have completed and the results have been collated, we can make smart comparisons about the results of our benchmarks.

Advertisement

In our output result, we can see a couple of different bits of data being returned:

The name of the benchmark that was run, followed by the following:

  • -4: The number of GOMAXPROCS that were used to execute the tests.
  • 101664: The number of times our loop ran to gather the necessary data.
  • 23184 ns/op: The speed per loop during our test.
  • PASS: Indicates the end state of our benchmark run.
  • The final line of the test, with a compilation of the end state of our test run (ok), the path that we ran the test on, and the total time of our test run.

 

Removing Setup from the Benchmark

For each iteration of the for loop, the BenchmarkSort function has to generate random data, and the time taken to produce this data is included in the benchmark results. We can use following methods for timing control:

  • StopTimer() : This method stops the timer.
  • StartTimer() : This method starts the timer.
  • ResetTimer() : This method resets the timer.

The ResetTimer method is useful when a benchmark requires some initial setup, and the other methods are useful when there is overhead associated with each benchmarked activity.

package main

import (
	"math/rand"
	"testing"
	"time"
)

func BenchmarkSort(b *testing.B) {
	rand.Seed(time.Now().UnixNano())
	size := 250
	data := make([]int, size)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		for j := 0; j < size; j++ {
			data[j] = rand.Int()
		}
		b.StartTimer()
		sortAndTotal(data)
	}
}

The timer is reset after the random seed is set and the slice has been initialized. Within the for loop, the StopTimer method is used to stop the timer before the slice is populated with random data, and the StartTimer method is used to start the timer before the sortAndTotal function is called.

# go test -bench . -run notest
goos: linux
goarch: amd64
cpu: Intel Core Processor (Haswell, no TSX, IBRS)
BenchmarkSort-4            52544             21956 ns/op
PASS
ok      _/opt/deepak/scripts/goexamples/tests   5.454s

Excluding the work required to prepare for the benchmark has produced a more accurate assessment of the time taken to execute the sortAndTotal function.

Advertisement

 

Summary

In this tutorial we learned about the golang benchmark and testing features.

While you are running the benchmarks in this book, be sure to remember that benchmarks aren't the be-all and end-all for performance results. Benchmarking has both positives and drawbacks:

The positives of benchmarking are as follows:

  • Surfaces potential problems before they become unwieldy
  • Helps developers have a deeper understanding of their code
  • Can identify potential bottlenecks in the design and data structures and algorithms stages

The drawbacks of benchmarking are as follows:

  • Needs to be completed on a given cadence for meaningful results
  • Data collation can be difficult
  • Does not always yield a meaningful result for the problem at hand

 

Categories GO

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment

X