Back to all entries

Blog

TechForce: Benchmark Go closures

Using closures comes with a performance costs compared to just using normal functions or anonymous functions. I benchmarked 3 scenarios for using closures, normal functions or anonymous functions.

  1. Using anonymous functions (inline functions that do not reference any variables outside).
  2. Using closures (referencing to variables outside of the function).
  3. Direct normal function calls.

Here is the benchmark I wrote

package benchmark_go_closures

import (
	"testing"
	"fmt"
)

func BenchmarkAnonymousFunction(b *testing.B) {
	var t int64
	for i := 0; i < b.N; i++ {
		f := func(j int64) (r int64) {
			r = j * 2
			return
		}

		t = f(int64(i))
	}
	fmt.Sprintln(t)
}

func BenchmarkClosure(b *testing.B) {
	var t int64
	for i := 0; i < b.N; i++ {
		f := func() (r int64) {
			r = int64(i) * 2
			return
		}
		t = f()
	}
	fmt.Sprintln(t)

}

func BenchmarkNormalFunction(b *testing.B) {
	var t int64
	for i := 0; i < b.N; i++ {
		t = multiple2(int64(i))
	}
	fmt.Sprintln(t)
}

func multiple2(i int64) (r int64) {
	r = i * 2
	return
}

Where the result I get is:

$ go test -test.bench=.*

testing: warning: no tests to run
PASS
BenchmarkAnonymousFunction	500000000	         3.27 ns/op
BenchmarkClosure	 5000000	       303 ns/op
BenchmarkNormalFunction	2000000000	         0.83 ns/op
ok  	github.com/sunfmin/benchmark_go_closures	5.532s

303 ns/op means that each loop takes 303 nanoseconds to complete, which is much longer than normal function calls (0.83 ns/op) and closures without reference outside variables (3.27 ns/op).

It appears Go has been somewhat optimised for anonymous functions, its speed is really close to normal function calls.

Appendix

To run the benchmark:

go test -test.bench=.*

To profile the benchmark:

$ go test -test.bench=Closure -test.cpuprofile=/tmp/benchcpu.prof

testing: warning: no tests to run
PASS
BenchmarkClosure	 5000000	       298 ns/op
ok  	github.com/sunfmin/benchmark_go_closures	1.817s



$ go test -test.bench=Closure -test.cpuprofile=/tmp/benchcpu.prof -c



$ go tool pprof benchmark_go_closures.test /tmp/benchcpu.prof
Welcome to pprof!  For help, type 'help'.
(pprof) web
Total: 152 samples
Loading web page file:////tmp/pprof8604.0.svg
(pprof) top10
Total: 152 samples
     145  95.4%  95.4%      151  99.3% fmt.(*operator++).printReflectValue
       4   2.6%  98.0%        4   2.6% runtime.MCache_Alloc
       2   1.3%  99.3%      150  98.7% runtime.closure
       1   0.7% 100.0%        1   0.7% fmt.(*operator++).printField
       0   0.0% 100.0%      141  92.8% fmt.(*operator++).handleMethods
(pprof) 

A few steps are needed to profile a benchmark:

  1. with -test.cupprofile to run benchmark generate profile data file.
  2. with -c option to generate the binary file
  3. run go tool pprof with the binary file and the profile data file.

You can also type web in pprof console to get a visual view of the call graph, which looks like this:

callgraph