Golang provide a way to know our code efficiency by benchmark our code. By benchmarking our code, we able to see how fast our code can run and even gain more insight.
An efficient code displayed from the resource required to complete it execution. The faster it can run, the better. The lower resource required, the better.
This concept also applied for codes that run with serverless platform such as AWS Lambda or Google Cloud Function. You are charged based on the number of requests for your functions and the duration it takes to execute.
Unless you have unlimited budget, you should have concerned more about: Does my code running efficiently?
Golang provide a way to know our code efficiency by benchmark our code. By benchmarking our code, we able to see how fast our code can run and even gain more insight.
Three Benefits of Benchmark:
- Measure the performance of your code. By running a benchmark, you can see how many iterations per second your code is able to run. This can help you identify performance bottlenecks and optimize your code.
- Easier to compare the performance of different implementations of a function. When you are in doubt, comparing two algorithm performance might help. Having a benchmark strengthen your argument.
- Ensure your code is correct. You can test your code under different conditions and input sizes. This can help you to find and fix bugs that might not appeared with smaller input or concurrence.
How to Benchmark
Go has a built-in function to perform benchmark on our own code. Here is how:
Creating Benchmark
Creating a benchmark in Go is a two-step process.
The first step is to create a benchmark function. To do a benchmark in Golang, you can use the testing
package. The testing
package has a Benchmark
function that you can use to run benchmark tests.
The next step is to write a test that calls the benchmark function.
Here is a code snippet as example:
package main
import (
"fmt"
"testing"
)
// BenchmarkExample is the core test, it called the function N times
func BenchmarkExample(b *testing.B) {
// run the function being tested b.N times
for n := 0; n < b.N; n++ {
// call the function
example()
}
}
func example() {
// this is the function being tested
fmt.Println("Hello World")
}
func main() {
// run the benchmark
testing.Benchmark(BenchmarkExample)
}
Here is the example folder structure:
benchmark/
├─ main_test.go
├─ go.mod
To run it, use a terminal and execute this:
go test -bench .
By using -bench
, the command will only perform test for benchmark test and skip the rest.
The function Hello World is too simple isn’t it? Let’s give a try with another example: Bubble Sort. You may see how the Bubble Sort works from running it in Golang Playground.
Here is our snippet code looks like:
package main
import "testing"
// BenchmarkExample is the core test, it called the function N times
func BenchmarkExample(b *testing.B) {
// generate input once, [0,1,2,3,4,5 ... 100]
input := makeSlice(0, 100, 1)
// run the function being tested b.N times
for n := 0; n < b.N; n++ {
// call the function
bubbleSortDesc(input)
}
}
func main() {
// run the benchmark
testing.Benchmark(BenchmarkExample)
}
// makeSlice generate slice from min to max with increment
func makeSlice(min, max, incr int) []int {
a := make([]int, max-min+1)
for i := range a {
a[i] = min + incr
}
return a
}
// bubbleSortDesc return a sorted descending slice
func bubbleSortDesc(arr []int) {
n := len(arr)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if arr[j] < arr[j+1] {
// Swap elements
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
}
*BubbleSort function was written by ChatGPT
Reading Benchmark’s Result
After write the test, let’s try to run the benchmark command. Lets use additional flag -benchmem
to verbose memory usage information.
go test -bench . -benchmem
After a while, we will see some output:
goos: darwin
goarch: arm64
pkg: example.com/m
BenchmarkBubbleSort-8 336618 3530 ns/op 0 B/op 0 allocs/op
PASS
ok example.com/m 2.380s
Here is how to read it:
goos
- operating system, its Darwin for MacOS.goarch
- CPU architecture, its arm64 as it ran using M1.pkg
- package name, depend on what we have set usinggo mod init
Now we see another line with five tabs:
BenchmarkExample-8
- name of the benchmark function, the8
suffix describe amount of CPU used.323156
- total number of the loop was executed,for n := 0; n < b.N; n++ {
, the higher the better.3685 ns/op
- time required to complete each loop on average. It written in nanoseconds per operation, the lower the better.0 B/op
- memory required each loop. It written in Bytes per operation, the lower the better.0 allocs/op
- memory allocations per operation, the lower the better.
The last two lines describe the state of the benchmark test and time required to complete all tests.
Compare Benchmark’s Result
Now that we have the Bubble Sort’s performance, we need another algorithm to compare with. Let’s use another popular sorting algorithm: Quick Sort.
Instead of rewrite the entire test file, we just need to add another test for Quick Sort.
Here is the final code snippet.
package main
import "testing"
// BenchmarkQuickSort is the quick sort test, it called the function N times
func BenchmarkQuickSort(b *testing.B) {
// generate input once, [0,1,2,3,4,5 ... 100]
input := makeSlice(0, 100, 1)
// run the function being tested b.N times
for n := 0; n < b.N; n++ {
// call the function
quickSortDesc(input)
}
}
// BenchmarkBubbleSort is the bubble sort test, it called the function N times
func BenchmarkBubbleSort(b *testing.B) {
// generate input once, [0,1,2,3,4,5 ... 100]
input := makeSlice(0, 100, 1)
// run the function being tested b.N times
for n := 0; n < b.N; n++ {
// call the function
bubbleSortDesc(input)
}
}
func main() {
// run the benchmark
testing.Benchmark(BenchmarkQuickSort)
testing.Benchmark(BenchmarkBubbleSort)
}
// makeSlice generate slice from min to max with increment
func makeSlice(min, max, incr int) []int {
a := make([]int, max-min+1)
for i := range a {
a[i] = min + incr
}
return a
}
// quickSortDesc return a sorted descending slice
func quickSortDesc(arr []int) []int {
if len(arr) <= 1 {
return arr
}
pivot := arr[len(arr)/2]
left := make([]int, 0, len(arr))
right := make([]int, 0, len(arr))
for _, v := range arr {
if v > pivot {
left = append(left, v)
} else if v < pivot {
right = append(right, v)
}
}
left = quickSortDesc(left)
right = quickSortDesc(right)
left = append(left, pivot)
left = append(left, right...)
return left
}
// bubbleSortDesc return a sorted descending slice
func bubbleSortDesc(arr []int) {
n := len(arr)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if arr[j] < arr[j+1] {
// Swap elements
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
}
*BubbleSort function was written by ChatGPT
After run the benchmark, we will have these kind of result:
goos: darwin
goarch: arm64
pkg: example.com/m
BenchmarkQuickSort-8 4066788 288.4 ns/op 1792 B/op 2 allocs/op
BenchmarkBubbleSort-8 322953 3501 ns/op 0 B/op 0 allocs/op
PASS
ok example.com/m 2.737s
As we already know, Quick Sort perform much more efficient result in given input. Time required to perform Bubble Sort is much longer than Quick Sort. Even with the cost of memory allocation, Quick Sort is clear winner.
But do remind, each algorithm has their best case. Here is another test result with input of a slice of 10 instead of 100:
goos: darwin
goarch: arm64
pkg: example.com/m
BenchmarkQuickSort-8 20601570 58.01 ns/op 192 B/op 2 allocs/op
BenchmarkBubbleSort-8 30120543 38.13 ns/op 0 B/op 0 allocs/op
PASS
ok example.com/m 3.778s
Now Bubble Sort is a winner. With lower input size, using Quick Sort is a bit slow but still have a great performance.
Final Words
By benchmarking our code, we able to see how fast our code can run and even gain more insight. Benchmark help you to identify you identify performance bottlenecks and optimize your code before the code being deployed.
With its benefits and simple implementation, Benchmark in Go is one of the high return investment that you should never underestimate.