Skip to content

cmd/compile: wrapping funcs in interfaces causes them to escape #74356

Open
@mcy

Description

@mcy

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

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions