Benchmarking Go code

Benchmarking is an essential part of writing efficient and performant code. It helps measure the speed and performance of the codebase, enabling developers to optimize critical sections and enhance overall execution time. In this article, we will explore how to benchmark Go code using the built-in testing package and a few best practices to get accurate results.

The Benchmarking Environment

Go provides a standardized testing framework that includes support for benchmarking. By convention, the benchmark functions are written in a file with a _test.go suffix and located in the same package as the code being tested.

To run benchmarks for a package, we use the go test command with the -bench flag followed by a regular expression that matches the benchmark functions' names. For example, if we have a benchmark function named BenchmarkSomething, we can run it using the following command:

go test -bench=Something ./...

Writing a Benchmark Function

A benchmark function follows a specific naming convention: it starts with the word Benchmark followed by the function or method being benchmarked. The function signature is as follows:

func BenchmarkSomething(b *testing.B) {
    // Benchmark code
}

The *testing.B parameter provides methods that control the benchmark's execution, like setting a number of iterations and measuring execution time.

Inside the benchmark function, we typically have a loop that performs the code being tested b.N times. The b.N value starts at a small number and exponentially grows over time until it stabilizes, allowing us to get accurate measurements without taking too long to execute.

func BenchmarkSomething(b *testing.B) {
    // Code setup
    
    for n := 0; n < b.N; n++ {
        // Code under benchmark
    }
    
    // Benchmark result
}

Measuring Execution Time

To measure execution time accurately, we can use the b.StartTimer() and b.StopTimer() methods. The b.StartTimer() function pauses the timer before the code under benchmark, and b.StopTimer() stops it.

func BenchmarkSomething(b *testing.B) {
    // Code setup
    
    b.ResetTimer()  // Reset the timer to exclude benchmark setup
    
    for n := 0; n < b.N; n++ {
        b.StartTimer()
        // Code under benchmark
        b.StopTimer()
    }
    
    // Benchmark result
}

By excluding the setup time from measurements, we ensure that only the code being benchmarked is considered.

Reporting Results

After executing the benchmark code, we can report the results using the b.ReportMetric() method. It allows us to provide additional metrics, such as memory consumption, disk utilization, or any other relevant information.

func BenchmarkSomething(b *testing.B) {
    // Code setup
    
    b.ResetTimer()
    
    for n := 0; n < b.N; n++ {
        b.StartTimer()
        // Code under benchmark
        b.StopTimer()
    }
    
    b.ReportMetric(float64(N), "iterations")
    b.ReportMetric(float64(b.N)/b.T, "op/s")
}

In the example above, we report the number of iterations (N) and operations per second (op/s).

Running Benchmarks

To run the benchmark functions, we use the go test command with the -bench flag. We can execute all the benchmarks in the current package by running:

go test -bench=. ./...

Or we can specify specific benchmarks to run by using a regular expression:

go test -bench=Something ./...

Once executed, Go will output the benchmark results, including timing information and any additional metrics reported.

Conclusion

Benchmarking is an invaluable tool when it comes to optimizing the performance of Go code. By following the naming convention, leveraging the testing framework, and using the *testing.B methods effectively, we can accurately measure execution time and identify potential bottlenecks.

Remember to run benchmarks regularly, especially after making performance-related changes, to ensure that the code remains efficient and responsive.


noob to master © copyleft