Consider the following program:
package p
import _ "unsafe"
func f[T any]() {}
func g[T any](n int) {
for range n {
escape(f[T])
}
}
//go:linkname escape
func escape(func())
var x = g[int]
g passes f[T] to escape each loop iteration. Because g is itself generic, it cannot use a pre-baked thunk that sets the dictionary for the call to f, so it must allocate a closure. This is all well and good and expected.
However, let's look at the assembly.
TEXT g[go.shape.int](SB)
PUSHQ BP
MOVQ SP, BP
SUBQ $32, SP
MOVQ AX, 48(SP)
JMP latch
backedge:
MOVQ CX, 8(AX)
CALL .h(SB)
MOVQ 16(SP), BX
DECQ BX
MOVQ 48(SP), AX
latch:
TESTQ BX, BX
JLE exit
MOVQ BX 16(SP)
MOVQ (AX), CX
MOVQ CX, 24(SP)
LEAQ type:noalg.struct { F uintptr; X0 *[0]uintptr }(SB), AX
CALL runtime.newobject(SB)
LEAQ .g[go.shape.int].func1(SB), CX
MOVQ CX, (AX)
CMPL runtime.writeBarrier(SB), $0 // NB: This write barrier seems redundant...?
JNE wb
MOVQ 24(SP), CX
JMP backedge
wb:
CALL runtime.gcWriteBarrier1(SB)
MOVQ 24(SP), CX
MOVQ CX, (R11)
JMP backedge
exit:
ADDQ $32, SP
POPQ BP
RET
Notice that every trip through this loop, we allocate a brand new closure, despite the fact that this closure does not "capture" anything, except for the (immutable!) type dictionary.
There is a broader issue that closures which capture "immutable state" (i.e., no capture has a use after closure creation, and the closure does not write to any capture, both of which are trivial to check in SSA) may still be constructed more than once. This general version is much harder to do and probably not quite as valuable.
However, LICM is not necessary to deal with this specific case. We know statically that g[T] requires a closure for f[T], so the type dictionary for g[T] should simply contain an entry for f[T]'s funcval thunk. This can be extended to include anonymous closures that capture the type dictionary (see #78370, which is tangentially related).
Consider the following program:
gpassesf[T]toescapeeach loop iteration. Becausegis itself generic, it cannot use a pre-baked thunk that sets the dictionary for the call tof, so it must allocate a closure. This is all well and good and expected.However, let's look at the assembly.
Notice that every trip through this loop, we allocate a brand new closure, despite the fact that this closure does not "capture" anything, except for the (immutable!) type dictionary.
There is a broader issue that closures which capture "immutable state" (i.e., no capture has a use after closure creation, and the closure does not write to any capture, both of which are trivial to check in SSA) may still be constructed more than once. This general version is much harder to do and probably not quite as valuable.
However, LICM is not necessary to deal with this specific case. We know statically that
g[T]requires a closure forf[T], so the type dictionary forg[T]should simply contain an entry forf[T]'s funcval thunk. This can be extended to include anonymous closures that capture the type dictionary (see #78370, which is tangentially related).