Inside a Go 'terrible hack' in the reflect package

October 16, 2015

Recently John Allspaw tweeted about some 'terrible hack' comments in the source code of various important projects. One of them was Go, which made me curious about the context. Although I can't exactly match Allspaw's version of the comment, I think it's this comment in src/reflect/value.go, which I'm going to quote in full:

func ValueOf(i interface{}) Value {
    [...]
    // TODO(rsc): Eliminate this terrible hack.
    // In the call to unpackEface, i.typ doesn't escape,
    // and i.word is an integer.  So it looks like
    // i doesn't escape.  But really it does,
    // because i.word is actually a pointer.
    escapes(i)

    return unpackEface(i)
}

In a nutshell, what's going on here is that the compiler is being too smart about escape analysis and the 'hack' code here is defeating that smartness in order to avoid memory errors.

Per Russ Cox's explanation on how interfaces are implemented, an interface{} value is represented in memory as essentially two pointers, one to the underlying type and one to the actual value. unpackEface() magically turns this into a reflect.Value, which has exactly this information (plus some internal stuff). Unfortunately it does so in a way that causes the compiler's escape analysis to think that nothing from the 'i' argument outlives ('escapes') unpackEface(), which would normally mean that the compiler thinks 'i' doesn't outlive ValueOf() either.

So let's imagine that you write:

type astruct struct { ... }
func toval() reflect.Value {
   var danger astruct
   return reflect.ValueOf(&danger)
}

Without the hack, escape analysis could tell the Go compiler that &danger doesn't escape reflect.ValueOf(), which would make danger safe to allocate on the stack, where it would get (implicitly) destroyed when toval() returns. Unfortunately the Value returned by toval() actually refers to this now-destroyed stack memory. Whoops. By explicitly defeating escape analysis, ValueOf() forces danger to be allocated in the heap where it will outlive toval() and thus avoid this bug.

(You might wonder if Go garbage collection has similar problems and the answer is apparently 'no', although the details are well beyond both me and the scope of this entry. See this golang-nuts thread on garbage collection and unsafe.Pointer.)

A Go compiler that was less smart about escape analysis wouldn't have this problem; as you can see, the compiler has to reason through several layers of code to go wrong. But escape analysis is an important optimization for a language like Go so the compiler has clearly worked hard at it.

(If the Go compiler is doing not just cross function but cross package escape analysis (which it certainly looks like), I have to say that I'm impressed by how thorough it's being.)

Sidebar: How escapes() works

Before I looked at it, I expected escapes() to involve some deep magic to tell the compiler to go away. The reality is more prosaic (and humbling for me in my flights of imagination):

var dummy struct {
    b bool
    x interface{}
}

func escapes(x interface{}) {
    if dummy.b {
       dummy.x = x
    }
}

In theory a sufficiently smart compiler could detect that dummy is not exported from reflect and is not touched inside it, so dummy.b is always false and escapes() always does nothing and so x does not escape it. In practice I suspect that the Go compiler will never get that perversely smart for various reasons.

Written on 16 October 2015.
« Some notes on finding package versions in OmniOS with pkg
Do generic stock servers have a future in a cloud world? »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Oct 16 02:54:40 2015
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.