Published in

justforfunc

# Analyzing the performance of Go functions with benchmarks

This blog post is complementary to episode 28 of justforfunc which you can watch right below.

In the previous blog post I discussed two different ways of merging n channels in Go, but we did not discuss which ones was faster. In the meanwhile a third way of merging channels was proposed in a YouTube comment.

This blog post will show that third way and compare all of them from a performance point of view, analyzed using benchmarks.

# A third way of merging channels

Two episodes ago we discussed how two channels could be merged, using a single goroutine and nil channels. We’ll refer to that function here as mergeTwo. A justforfunc viewer proposed a way to use this function and recursion to provide a way to merge n channels.

The solution is quite smart. If we have:

• one channel, we return that channel.
• two channels or more, we merge a half of the channels and then merge them using the result of those using the efficient function.

Written in code it would look like this:

This is a nice solution that avoids using reflection and also reduces the number of goroutines needed. On the other hand, it uses more channels than before!

So, which one is fastest? Time for benchmarks!

# Writing a benchmark

Testing and benchmarks are integrated very tightly with the Go tooling, and writing a benchmark is as simple as writing a function with a name starting with `Benchmark` and the appropriate signature.

The only parameter a benchmark receives is of type `testing.B` and it defines the number of times we should perform the operation we’re benchmarking. This is done for statistical significance, by default benchmarks are given a number of iterations that will make the function run for a whole second.

Ok, so now we know pretty much everything we need to know to write a benchmark for one of our functions.

# Benchmarking all our functions

Ok, so we’re ready to write benchmarks for each function!

One for `merge`:

One for `mergeReflect`:

And finally. one for `mergeRec`:

And now we can finally run them!

`➜ go test -bench=.goos: darwingoarch: amd64pkg: github.com/campoy/justforfunc/27-merging-chansBenchmarkMerge-4                  200000              7074 ns/opBenchmarkMergeReflect-4           100000             11904 ns/opBenchmarkMergeRec-4               500000              2475 ns/opPASSok      github.com/campoy/justforfunc/27-merging-chans  4.077s`

Great, so using recursion is the fastest possible option. Let’s believe it for now!

# Using sub-benchmarks and b.Run

Unfortunately, in order to benchmark three functions we’re repeating lots of code. Wouldn’t it be nice if we could write our benchmark once?

We can write a function that iterates over our three functions:

Unfortunately, that simply gives a single and pretty meaningless measure, mixing all performances.

`➜ go test -bench=.goos: darwingoarch: amd64pkg: github.com/campoy/justforfunc/27-merging-chansBenchmarkMerge-4           50000             22440 ns/opPASSok      github.com/campoy/justforfunc/27-merging-chans  1.386s`

Luckily, we can easily create subbenchmarks by calling `testing.B.Run`, similarly as subtests with `testing.T.Run`. In order to do so, we need a name, which is actually a pretty good idea from a documentation point of view, anyway.

When we run these benchmarks we’ll now see three different results as we wished.

`➜ go test -bench=.goos: darwingoarch: amd64pkg: github.com/campoy/justforfunc/27-merging-chansBenchmarkMerge/goroutines-4               200000          6145 ns/opBenchmarkMerge/reflect-4                  200000         10962 ns/opBenchmarkMerge/recursion-4                500000          2368 ns/opPASSok      github.com/campoy/justforfunc/27-merging-chans  4.829s`

Sweet! With way less code we’re still able to benchmark all of our functions.

## Benchmarking multiple values of N

So far we’ve seen that for a single channel the recursive algorithm is the fastest, but what about other values of `N`?

We could probably just change the value of `N` many times and see how it goes. But we could also just use a for loop!

In the code below we iterate over the powers of 2 from 1 to 1024, and use a combination of the function we’re benchmarking and the value of `N` as the benchmark name.

Let’s run the benchmarks!

`➜ go test -bench=.goos: darwingoarch: amd64pkg: github.com/campoy/justforfunc/27-merging-chansBenchmarkMerge/goroutines/1-4             200000          6919 ns/opBenchmarkMerge/goroutines/2-4             100000         13212 ns/opBenchmarkMerge/goroutines/4-4              50000         25469 ns/opBenchmarkMerge/goroutines/8-4              30000         50819 ns/opBenchmarkMerge/goroutines/16-4             20000         88566 ns/opBenchmarkMerge/goroutines/32-4             10000        162391 ns/opBenchmarkMerge/goroutines/64-4              5000        299955 ns/opBenchmarkMerge/goroutines/128-4             3000        574043 ns/opBenchmarkMerge/goroutines/256-4             1000       1129372 ns/opBenchmarkMerge/goroutines/512-4             1000       2251411 ns/opBenchmarkMerge/goroutines/1024-4             300       4760560 ns/opBenchmarkMerge/reflect/1-4                200000         10868 ns/opBenchmarkMerge/reflect/2-4                100000         22335 ns/opBenchmarkMerge/reflect/4-4                 30000         54882 ns/opBenchmarkMerge/reflect/8-4                 10000        148218 ns/opBenchmarkMerge/reflect/16-4                 3000        543921 ns/opBenchmarkMerge/reflect/32-4                 1000       1694021 ns/opBenchmarkMerge/reflect/64-4                  200       6102920 ns/opBenchmarkMerge/reflect/128-4                 100      22648976 ns/opBenchmarkMerge/reflect/256-4                  20      90204929 ns/opBenchmarkMerge/reflect/512-4                   3     383579039 ns/opBenchmarkMerge/reflect/1024-4                  1    1676544681 ns/opBenchmarkMerge/recursion/1-4              500000          2658 ns/opBenchmarkMerge/recursion/2-4              100000         14707 ns/opBenchmarkMerge/recursion/4-4               30000         44520 ns/opBenchmarkMerge/recursion/8-4               10000        114676 ns/opBenchmarkMerge/recursion/16-4               5000        261880 ns/opBenchmarkMerge/recursion/32-4               3000        560284 ns/opBenchmarkMerge/recursion/64-4               2000       1117642 ns/opBenchmarkMerge/recursion/128-4              1000       2242910 ns/opBenchmarkMerge/recursion/256-4               300       4784719 ns/opBenchmarkMerge/recursion/512-4               100      10044186 ns/opBenchmarkMerge/recursion/1024-4              100      20599475 ns/opPASSok      github.com/campoy/justforfunc/27-merging-chans  61.533s`

We can see that even though the recursive algorithm is the fastest for a single channel, very quick the solution with many goroutines outperforms the others.

We can also this by plotting these numbers with some quick Python. Stop acting shocking I somtimes write in Python too!

It’s hard to see the difference, so let’s use a logarithmic scale for the Y axis (simply add `plt.yscale('log')`).

# Managing the benchmark timer

There’s an obvious issue with the way we’re measuring performance. We’re also counting the time it takes to create the `N` channels for our tests, which adds cost that is not relevant to our algorithms.

We can avoid this by using `b.StopTimer()` and `b.StartTimer()`.

The results are not that different to our previous benchmark, but we can see the timing difference increases with the value of `N`.

# Can we make them faster?

We finally have a good view of how each function performs for different sizes of `N`. This is great, but doesn’t really help us making our functions faster.

How could we know more? Well, benchmarks take us only so far, but the next tool we we can use is profiling! Wanna know more? Next episode coming up in two weeks.

# Thanks

If you enjoyed this episode make sure you share it and subscribe to justforfunc! Also, consider sponsoring the series on patreon.

--

--

--

## More from justforfunc

The blog posts related to the justforfunc YouTube series that you can watch on justforfunc.com

## Francesc Campoy

VP of Product at Dgraph 🏳️‍🌈 Catalan