6 minute read

How fast is Go’s defer?

Last week, I was writing a very basic endless runner with the SDL bindings for Go. Texture, surfaces, fonts, windows (and a few more) are all resources that you will need to free. If you procedurally generate some of those, chances are you’re going to want to defer freeing those resources, and some of this will most likely happen in a loop.

This can be extremely convenient from a code organization perspective. But how much of an impact does deferring a method call have on performance?

My guess was: not much, but…

“Never guess what can be estimated.”

— George H. Bigelow

So I went ahead and wrote this simple program to see how much of an impact defer has.

package main

import "flag"

func inc() {
    // Fake some work
    _ = 1 + 1
}

func main() {
    var useDefer = flag.Bool("defer", false, "Use defered function call")
    flag.Parse()

    if *useDefer {
        for i := 0; i < 10000000; i++ {
            defer inc()
        }
    } else {
        for i := 0; i < 10000000; i++ {
            inc()
        }
    }
}
$> time ./benchmark
./benchmark  0.00s user 0.00s system 68% cpu 0.011 total

$> time ./benchmark -defer
./benchmark -defer  1.01s user 0.26s system 93% cpu 1.364 total

Now, this benchmark isn’t very representative of a real-world workload. The function we defer is very inexpensive, and we call it 10 million times.

As the deferred function grows more expensive, the overhead of deferring becomes less and less of a burden.

The overhead is in the order of magnitude of nanoseconds. Still, calling a function through defer is roughly 100 times slower than inlining it.

I was going to stop the article at this point, but it got me thinking. What is actually involved when calling defer? Can we somehow reproduce this behavior?

Reproducing defer’s behavior’

Let’s look at the Go spec:

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.

To paraphrase: when called with defer function gets pushed on a call stack along with its arguments. Before the calling function exits, every function on that stack is popped one-by-one and executed.

Let’s see this in action.

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        defer func(i int) { fmt.Println(i) }(i)
    }
}

// Output:
4
3
2
1

Now that we understand what defer does, time to reproduce its behavior!

To simplify things a bit a lot, we’ll only allow deferring functions with an arity of 0 and not returning anything. We’ll need a method to defer execution and one that we will call explicitly before returning to execute everything we deferred.

package deferstack

type simpleFunc func()

type DeferStack struct {
    funcs []simpleFunc
}

func (*DeferStack) NewDeferStack() {
    s := DeferStack{}
    s.funcs = make([]simpleFunc, 0, 10000000)

    return &s
}

func (s *DeferStack) Defer(f simpleFunc) {
    s.funcs = append(s, f)
}

func (s *DeferStack) Execute() {
    for i := len(s.funcs) - 1; i >= 0; i-- {
        s[i]()
    }
}

Now we can write a new benchmarking program to use DeferStack, and compare it with the defer implementation Go provides.

package main

import (
    "flag"

    "./deferstack"
)

func main() {
    var useDefer = flag.Bool("go-defer", false, "Use Go's defer")
    flag.Parse()

    if *useDefer {
        for i := 0; i < 10000000; i++ {
            defer func(i int) { _ = i + 1 }(i)
        }
    } else {
        ds := deferstack.NewDeferStack()
        for i := 0; i < 10000000; i++ {
            func(i int) {
                ds.Defer(func() { _ = i + 1 })
            }(i)
        }
        ds.Execute()
    }
}
$> time ./benchmark -go-defer
./benchmark -go-defer  0.95s user 0.30s system 96% cpu 1.282 total
$> time ./benchmark
./benchmark  0.86s user 0.13s system 129% cpu 0.762 total

This naive implementation is still much faster than Go’s defer. Given the shortcuts we took, I’m really not that surprised, but let’s see if we can get to something more realistic.

inceptionweneedtogodeeper.gif

Since we know how many function-calls we want to defer, we are allocating enough memory for the defer stack up front. This has two very important effects on speed:

  • No further memory allocation is required
  • Since the slice doesn’t grow in capacity, there’s no need to copy the data from a previous slice.

In most cases, there’s no way to know at compile-time how many functions are going to be deferred.

Using a slice without a pre-defined size is also sub-optimal. When a new item needs to be appended to a slice at full capacity:

  • A new slice is allocated with a capacity equal to the next power of two.
  • Elements from the old slice are then copied over to the new slice.

This takes time. For a construct in which the number of items is usually pretty small, a linked list is a much more appropriate data structure.

Let’s peek under the hood and see if we’re right1:

struct Defer
{
    int32   siz;        // Number of args
    bool    started;    //   
    uintptr argp;       // Where args were copied from
    uintptr pc;         // Pointer to code to be executed
    FuncVal*    fn;     // Pointer to calling function (not sure)
    Panic*  panic;      // Panic that is running defer
    Defer*  link;       // Pointer to next Defer structure
};

Let’s modify our DeferStack implementation to use a linked list.

package deferstack

type simpleFunc func()

type DeferStack struct {
    funcs *deferred
}

type deferred struct {
    f    simpleFunc
    next *deferred
}

func NewDeferStack() *DeferStack {
    s := DeferStack{}
    return &s
}

// New deferred fuctions go to the beginning of the list
func (s *DeferStack) Defer(f simpleFunc) {
    s.funcs = &deferred{f: f, next: s.funcs}
}

func (s *DeferStack) Execute() {
    for deferred := s.funcs; deferred != nil; deferred = deferred.next {
        deferred.f()
    }
}

We changed the slice-based implementation to one using a singly linked list. We did that without breaking our code’s interface, so we don’t need to change anything else, nice!

Time to measure performance again:

$> time ./benchmark -go-defer
./benchmark -go-defer  1.02s user 0.30s system 96% cpu 1.366 total
$> time ./benchmark
./benchmark  1.77s user 0.34s system 129% cpu 1.626 total

Now we’re talking! The new implementation is slightly slower than the one provided by Go, but we have something in the same vein as the original.

Go’s implementation uses compiler and assembly tricks2 to optimize the execution of deferred functions.

Conclusion

How much of an impact does deferring a method call have on performance?

We’re now quite far from the original question, but we’ve learned a lot! Understanding the underlying structure of defer will help when making the decision to use it, or not.

Deferring a function call isn’t free, but given what actually happens under the hood, it’s pretty damn fast!

The Go team at Google is full of smart people who have been working on the language for 8 years, and this was hacked together pretty quickly. Because of that, I don’t feel too bad about the performance difference and feature disparity.

Notes:

1 - This was removed in 2014 as part of an effort to remove C headers from Go, that’s the last easily traceable reference I could find to the Defer structure.

2 - https://research.swtch.com/goabstract

Say something

Comments

Nothing yet.