How to Write Benchmark Tests for Your Golang Functions
Hello Gophers đź‘‹
Let me start by asking you a question: How would you test the performance of a piece of code or a function in Go? Well, you could use benchmark tests.
In this tutorial, I will show you how to use an awesome benchmarking tool that’s built into the Golang testing package.
Let’s go.
What Are Benchmark Tests?
In Go, benchmark tests are used to measure the performance (speed and memory usage) of functions or blocks of code. These tests are part of the Go testing framework and are written in the same files as unit tests, but they are specifically for performance analysis.
Example Use Case: Fibonacci Sequence
For this example, I'll be using the classic Fibonacci Sequence, which is determined by:
if (x < 2)
F(0) = 1
F(2) = 2
else
F(x) = F(x-1) + F(x-2)
In practice, the sequence is:
1, 1, 2, 3, 5, 8, 13, etc.
This sequence is important because it appears in various parts of mathematics and nature as well, as shown below:
There are several ways to implement this code, and I'll be picking two of them for our benchmark testing: the recursive and iterative methods. The main objective of the functions is to provide a position and return the Fibonacci number at that position.
Recursive Method
// main.go
func fibRecursive(n uint) uint {
if n <= 2 {
return 1
}
return fibRecursive(n-1) + fibRecursive(n-2)
}
The function above is a recursive implementation of calculating the Fibonacci sequence. Now I’ll break it down step by step for you as a beginner in Go.
Here’s your function for calculating the Fibonacci numbers:
func fibRecursive(n uint) uint {
if n <= 2 {
return 1
}
return fibRecursive(n-1) + fibRecursive(n-2)
}
1. Function:
func fibRecursive(n uint) uint
func
: This keyword defines a function in Go.fibRecursive
: This is the name of the function. It’s calledfibRecursive
because it calculates Fibonacci numbers using recursion.n uint
: The function takes a single argument,n
, which is of typeuint
(an unsigned integer). This represents the position of the Fibonacci sequence that we want to calculate.uint
: The function returns auint
(unsigned integer) because Fibonacci numbers are non-negative integers.
2. Base Stage:
if n <= 2 {
return 1
}
The
if
statement checks ifn
is less than or equal to 2.In the Fibonacci sequence, the 1st and 2nd numbers are both 1. So, if
n
is 1 or 2, the function returns 1.This is called the base stage, and it stops the recursion from going infinitely deep.
3. Recursive Stage:
return fibRecursive(n-1) + fibRecursive(n-2)
If
n
is greater than 2, the function calls itself twice:fibRecursive(n-1)
: This will calculate the Fibonacci number for the position just beforen
.fibRecursive(n-2)
: This will calculate the Fibonacci number for two positions beforen
.
The function then adds these two results together, because every Fibonacci number is the sum of the two preceding numbers.
For more theory on recursion, check out these articles.
Iterative Method
// main.go
func fibIterative(position uint) uint {
slc := make([]uint, position)
slc[0] = 1
slc[1] = 1
if position <= 2 {
return 1
}
var result, i uint
for i = 2; i < position; i++ {
result = slc[i-1] + slc[i-2]
slc[i] = result
}
return result
}
This code implements an iterative approach to calculate the Fibonacci sequence in Go, which is different from the recursive approach. Here’s a breakdown of how it works:
1. Function:
func fibIterative(position uint) uint
func
: This keyword declares a function in Go.fibIterative
: The name of the function suggests that it calculates Fibonacci numbers using iteration (a loop).position uint
: The function takes one argument,position
, which is an unsigned integer (uint
). This represents the position of the Fibonacci sequence you want to calculate.uint
: The function returns an unsigned integer (uint
), which will be the Fibonacci number at the specified position.
2. Creating a Slice (Array-like structure):
slc := make([]uint, position)
slc
is a slice (a dynamic array in Go) that is created with the length ofposition
. This slice will store Fibonacci numbers at each index.
3. Initial Values for Fibonacci Sequence:
slc[0] = 1
slc[1] = 1
- The first two Fibonacci numbers are both
1
, so the first two positions in the slice (slc[0]
andslc[1]
) are set to1
.
4. Early Return for Small Positions:
if position <= 2 {
return 1
}
- If the input
position
is1
or2
, the function directly returns1
, because the first two Fibonacci numbers are always1
.
5. Iterative Loop:
var result, i uint
for i = 2; i < position; i++ {
result = slc[i-1] + slc[i-2]
slc[i] = result
}
The loop starts from
i = 2
and runs until it reaches theposition
.In each iteration, the Fibonacci number at index
i
is calculated as the sum of the two previous Fibonacci numbers (slc[i-1]
andslc[i-2]
).The result is stored both in
result
and in the sliceslc[i]
for future calculations.
6. Returning the Result:
return result
- Once the loop finishes, the variable
result
holds the Fibonacci number at the desired position, and the function returns it.
This is a more efficient approach to calculating Fibonacci numbers compared to recursion, especially when position
is large, because it doesn’t repeat unnecessary calculations and we are proving by using benchmark tests. Let’s prove it.
How to Run the Benchmark Tests
Now, for the benchmark tests, let’s write some test. First, you will need to create a maintest.go file. In it, using Golang's documentation on benchmark tests, you can create the functions to be tested as follows:
// main_test.go
// Benchmark for Iterative Function
func BenchmarkFibIterative(b *testing.B) {
for i := 0; i < b.N; i++ {
fibIterative(uint(10))
}
}
// Benchmark for Recursive Function
func BenchmarkFibRecursive(b *testing.B) {
for i := 0; i < b.N; i++ {
fibRecursive(uint(10))
}
}
Let's run the test for position 10 and then increase appropriately. To run the benchmark tests, you simply run the command go test -bench=NameoftheFunction
.
If you want to know more about this command, check here. Let’s check the function for position 10:
func BenchmarkFibIterative(b *testing.B) {
for i := 0; i < b.N; i++ {
fibIterative(uint(10))
}
}
go test -bench=BenchmarkFibIterative
Results:
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFibIterative-8 27715262 42.86 ns/op
PASS
ok playground 2.617s
Let’s analyze with the help of this image:
According to the image, we have 8 cores for the tests, and no time limit (it will run until completion). It took 27_715_262 iterations and 1.651 seconds to complete the task.
func BenchmarkFibRecursive(b *testing.B) {
for i := 0; i < b.N; i++ {
fibRecursive(uint(10))
}
}
go test -bench=BenchmarkFibRecursive
Results:
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFibRecursive-8 6644950 174.3 ns/op
PASS
ok playground 1.819s
Using the same image to analyze the result, in this case it took 6_644_950 iterations and 1.819 seconds to complete the task we have:
Fibonacci’s Function | Position | Iterations | Time to run (s) |
Iterative | 10 | 27_715_262 | 1.651 |
Recursive | 10 | 6_644_950 | 1.819 |
The benchmark results show that the iterative approach is significantly more efficient than the recursive approach for calculating the Fibonacci sequence.
For position 10, the iterative function ran approximately 27.7 million iterations in 1.651 seconds, while the recursive function managed only 6.6 million iterations in 1.819 seconds. The iterative method outperformed the recursive method both in terms of iterations and time, highlighting its efficiency.
To proven even further this, let’s try with the position 40 (4 times the previous value):
// Results for the Iterative Function
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFibIterative-8 9904401 114.5 ns/op
PASS
ok playground 1.741s
// Results for the Recursive Function
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFibRecursive-8 4 324133575 ns/op
PASS
ok playground 3.782s
Fibonacci’s Function | Position | Iterations | Time to run (s) |
Iterative | 40 | 9_904_401 | 1.741 |
Recursive | 40 | 4 | 3.782 |
The benchmark results clearly highlight the efficiency difference between the iterative and recursive approaches for calculating Fibonacci again.
The iterative function completed approximately 9.9 million iterations with an average execution time of 114.5 nanoseconds per operation, finishing the benchmark in 1.741 seconds. In stark contrast, the recursive function only completed 4 iterations with an average execution time of 324,133,575 nanoseconds per operation (over 324 milliseconds per call), taking 3.782 seconds to finish.
These results demonstrate that the recursive approach is far less efficient due to repeated function calls and recalculations, making the iterative method vastly superior in both speed and resource usage, especially as input size increases.
Just out of curiosity, I tried position 60 and it literally crashed the test:
// Results for the Iterative Function
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFibIterative-8 7100899 160.9 ns/op
// Results for the Recursive Function
SIGQUIT: quit
PC=0x7ff81935f08e m=0 sigcode=0
goroutine 0 gp=0x3bf1800 m=0 mp=0x3bf26a0 [idle]:
runtime.pthread_cond_wait(0x3bf2be0, 0x3bf2ba0)
...
Conclusion
If your production code is running slowly or is unpredictably slower, you can use this technique, combined with pprof or other tools from the built-in testing package, to identify and test where your code is performing poorly and work on how to optimize it.
Remember: Code that is beautiful to the eyes is not necessarily more performant.
Reference
Homework
This article explains why for some small numbers, the recursive strategy is better. Can you find a better way to improve the recursive function? (Tip: use Dynamic Programming).
Subscribe to my newsletter
Read articles from Pedro Bertao directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by