Why basic interface in type parameters causes resulting monomorphic function to use runtime.assertI2I()?

Viewed 94

Polymorphic function (full code see here):

type Intf interface{ 
    Do() 
}
type Intf2 interface {
    Intf
    Do2()
}

func CallIntf[T Intf](intf T) {
    intf.Do()
}

Manual monomorph:

func CallIntf_mono(intf Intf) {
    intf.Do()
}

Instantiation:

    var intf2 Intf2
    intf2 = &impl{}
    CallIntf[Intf](intf2)

Instantiation asm (contains a call to runtime.assertI2I()):

    LEAQ    type."".impl(SB), AX
    CALL    runtime.newobject(SB)
    MOVQ    AX, ""..autotmp_13+24(SP)
    LEAQ    go.itab.*"".impl,"".Intf2(SB), BX
    LEAQ    type."".Intf(SB), AX
    CALL    runtime.convI2I(SB)
    MOVQ    AX, BX
    MOVQ    ""..autotmp_13+24(SP), CX
    LEAQ    ""..dict.CallIntf["".Intf](SB), AX
    CALL    "".CallIntf[go.shape.interface { Do() }_0](SB)

Generated monomorphic function asm (contains a call to runtime.assertI2I()):

    TEXT    "".CallIntf[go.shape.interface { Do() }_0](SB), DUPOK|ABIInternal, $32-24
    MOVQ    CX, "".intf+56(SP)
    LEAQ    type."".Intf(SB), AX
    CALL    runtime.assertI2I(SB)
    MOVQ    24(AX), CX
    MOVQ    "".intf+56(SP), AX
    CALL    CX
    MOVQ    24(SP), BP
    ADDQ    $32, SP
    RET

Manual monomorph asm (does not call runtime.assertI2I()):

    TEXT    "".CallIntf_mono(SB), ABIInternal, $16-16
    MOVQ    AX, "".intf+24(FP)
    MOVQ    BX, "".intf+32(FP)
    MOVQ    24(AX), CX
    MOVQ    BX, AX
    CALL    CX
    MOVQ    8(SP), BP
    ADDQ    $16, SP
    RET

Question: Why does the assembly of the generated monomorphic function use runtime.assertI2I(), while the one of the manual monomorphic function does not? In which case caller would use a type which needs to be asserted?

1 Answers

I couldn't get confirmation elsewhere, so I'm just going to post my intuition here.

What you see is likely due to the current implementation of Go generics, which is based on GC Stenciling. GC stands for Garbage Collector.

The executive summary is:

[...], for simplicity (and performance) of implementation, we do not have a single compilation of a generic function/method for all possible type arguments. Instead, we share an instantiation of a generic function/method among sets of type arguments that have the same gcshape.

A gcshape is a group of types that "look the same" to the garbage collector. More formally, types that have the same underlying type, or pointers.

The identifier go.shape.interface { Do() }_0 seen in your ASM is the gcshape of the Intf interface, used as a type parameter constraint, where go.shape is a builtin package and interface { Do() } is the underlying type of Intf.

Based on my understanding, the above implies that generic functions aren't truly monomorphized upon instantiation. So the pseudo-register SB ends up having the gcshape of the interface, not the interface itself. For this reason, runtime.assertI2I(SB) is used to ensure that the two interfaces are compatible.

Anyway this is all implementation details. The ASM may change with future Go releases, and even point releases. At the time of writing, the assertion is still present in Go 1.18.3 compiled code.

Related