Description
Consider the following program:
package x
type i interface { f(*int) }
type f func(*int)
func (f f) f(x *int) { f(x) }
//go:noinline
func y1(i i) {
i.f(nil)
}
//go:noinline
func y2(f func(*int)) {
f(nil)
}
func z(n int) {
y1(f(func(y *int) { *y = n }))
y2(func(y *int) { *y = n })
}
i
is a single method interface; f
is the classic idiom for converting a func into such an interface, which is completely free, because funcs are stored as direct interface values.
We pass two versions of the same function into functions that call it with nil: once as an interface, and once as a func. One would think these generate identical code. However, escape analysis gets confused and concludes that the argument to y1
escapes, but not the argument to y2
, as seen in this assembly listing (go1.24.2):
TEXT x.z(SB), ABIInternal, $40-8
CMPQ SP, 16(R14)
JLS ...
PUSHQ BP
MOVQ SP, BP
SUBQ $32, SP
MOVQ AX, 48(SP)
LEAQ type:noalg.struct { ... }(SB), AX
CALL runtime.newobject(SB)
LEAQ x.z.func1(SB), CX
MOVQ CX, (AX)
MOVQ x.n+48(SP), CX
MOVQ CX, 8(AX)
MOVQ AX, BX
LEAQ go:itab.x.f,x.i(SB), AX
NOP
CALL x.y1(SB)
LEAQ x.z.func2(SB), CX
MOVQ CX, 16(SP)
MOVQ 48(SP), CX
MOVQ CX, +24(SP)
LEAQ 16(SP), AX
CALL x.y2(SB)
ADDQ $32, SP
POPQ BP
RET
The compiler chooses to spill one funcval to the heap but not the other. This is very surprising to me, and either indicates a bug in how escape analysis reasons through interfaces, or something that the current analysis is not smart enough to handle.
In any case, I discovered this because a similar variant of this code appears to cause some inlining budget to be exhausted: in a function that used to do nothing but return a closure, wrapping the closure in an interface conversion causes it to no longer inline. I cannot get that to reproduce and instead discovered this bug while building a POC, which may be the root cause.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status