pwshub.com

How to Write Benchmark Tests for Your Golang Functions

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:

Fibonacci sequence in a spiral (like a snail shell)

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 called fibRecursive because it calculates Fibonacci numbers using recursion.

  • n uint: The function takes a single argument, n, which is of type uint (an unsigned integer). This represents the position of the Fibonacci sequence that we want to calculate.

  • uint: The function returns a uint (unsigned integer) because Fibonacci numbers are non-negative integers.

2. Base Stage:

if n <= 2 {
    return 1
}
  • The if statement checks if n 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 before n.

    • fibRecursive(n-2): This will calculate the Fibonacci number for two positions before n.

  • 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 of position. 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] and slc[1]) are set to 1.

4. Early Return for Small Positions:

if position <= 2 {
    return 1
}
  • If the input position is 1 or 2, the function directly returns 1, because the first two Fibonacci numbers are always 1.

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 the position.

  • In each iteration, the Fibonacci number at index i is calculated as the sum of the two previous Fibonacci numbers (slc[i-1] and slc[i-2]).

  • The result is stored both in result and in the slice slc[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.

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:

visit https://www.practical-go-lessons.com/chap-34-benchmarks

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 FunctionPositionIterationsTime to run (s)
Iterative1027_715_2621.651
Recursive106_644_9501.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 FunctionPositionIterationsTime to run (s)
Iterative409_904_4011.741
Recursive4043.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

  • Recursive & Iterative functions to Fibonacci’s sequence here.

  • Benchmark testing here.

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).

Source: freecodecamp.org

Related stories
1 month ago - Get a sneak peek at the upcoming features in Python 3.13 aimed at enhancing performance. In this tutorial, you'll make a custom Python build with Docker to enable free threading and an experimental JIT compiler. Along the way, you'll...
2 weeks ago - In this tutorial, you'll learn about the new features in Python 3.13. You'll take a tour of the new REPL and error messages and see how you can try out the experimental free threading and JIT versions of Python 3.13 yourself.
12 hours ago - SEO monitoring tools are essential for tracking website performance, identifying issues, and optimizing search rankings by providing real-time data and insights. They provide valuable insights into keyword effectiveness, competitor...
1 week ago - Our next major version of Deno combines the simplicity, security, and performance of Deno 1 with full Node and npm backwards compatibility, and much more.
1 month ago - Bun now supports compiling and running C from JavaScript to make using systems libraries easier.
Other stories
24 minutes ago - IdPs (aka Identity providers) are crucial in the modern digital world. Learn what they are and what they do.
2 hours ago - Mobile app development has evolved tremendously, and creating a robust, full-featured app today involves mastering both the front-end and back-end. If you're looking to build something practical, like an e-commerce platform, and want to...
2 hours ago - While it might seem challenging to remain SOC 2 compliant, it is a critical process that helps earn your client’s trust and also ensures the security of your systems. SOC 2 assesses how well a company protects its data based on five trust...
2 hours ago - Building fullstack web applications is one of the most valuable skills in modern web development. If you're looking for a project that can take you through the complete development process—from setting up the front end to managing a...
3 hours ago - A playful concept where we use staggered scroll-based animations to create 3D grid effects and other transitions.