Description
Consider the following program:
package x
func z(x int) (*int, int) {
switch x := any(x).(type) {
case *int:
return x, 0
case int:
return nil, x
default:
return nil, 0
}
}
One might imagine this compiles to the following:
TEXT x.z
MOVD R0, R1
MOVD ZR, R0
RET
Unfortunately, it actually performs an allocation:
TEXT x.z(SB), ABIInternal, $32-8
MOVD 16(g), R16
CMP R16, RSP
BLS ...
PCDATA $0, $-1
MOVD.W R30, -32(RSP)
MOVD R29, -8(RSP)
SUB $8, RSP, R29
CALL runtime.convT64(SB)
MOVD (R0), R1
MOVD ZR, R0
MOVD -8(RSP), R29
MOVD.P 32(RSP), R30
RET (R30)
It is fair to argue that this is a duplicate of #74364, since it's ultimately because the compiler does not perform flow-sensitive escape analysis. However, I believe this case is more egregious and actually represents a compiler phase ordering problem. Once we lower the type switch into a series of branches (presumably in SSA) and delete all of the unreachable branches, we have already decided that any(x)
escapes, and must be heap-allocated.
My original reproduction was about a function containing such a type switch (consider protoreflect.ValueOf()
) unconditionally escaping its any
argument, which is unfortunately common in pre-generic code:
package x
func y(x any) (*int, int) {
switch x := x.(type) {
case *int:
return x, 0
case int:
return nil, x
default:
return nil, 0
}
}
func z(x int) (*int, int) {
return y(x)
}
I was originally planning to argue that inlining should be able to provide a discount to the inlining budget when the callee contains a type switch that, if inlined, could be devirtualized by the callee, since I believed this would eliminate the allocation, side-stepping any inter-procedural escape analysis issues.
Unfortunately, I hit this escape analysis miss, and I don't think that the above inlining suggestion makes sense until this bug is fixed. That said, I'm happy to file an issue about the optimization I suggest above on request.