2015-10-16
Inside a Go 'terrible hack' in the reflect
package
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.