go: cmd/compile: generic function argument causes escape to heap
What version of Go are you using (go version)?
$ go version go version devel go1.18-7c572a29eb Sat Oct 2 22:06:39 2021 +0100 darwin/arm64
Does this issue reproduce with the latest release?
Yes, needs generics.
What operating system and processor architecture are you using (go env)?
go env Output
$ go env GO111MODULE="" GOARCH="arm64" GOBIN="" GOCACHE="/Users/bryan/Library/Caches/go-build" GOENV="/Users/bryan/Library/Application Support/go/env" GOEXE="" GOEXPERIMENT="" GOFLAGS="" GOHOSTARCH="arm64" GOHOSTOS="darwin" GOINSECURE="" GOMODCACHE="/Users/bryan/go/pkg/mod" GONOPROXY="" GONOSUMDB="" GOOS="darwin" GOPATH="/Users/bryan/go" GOPRIVATE="" GOPROXY="https://proxy.golang.org,direct" GOROOT="/Users/bryan/src/github.com/golang/go" GOSUMDB="sum.golang.org" GOTMPDIR="" GOTOOLDIR="/Users/bryan/src/github.com/golang/go/pkg/tool/darwin_arm64" GOVCS="" GOVERSION="devel go1.18-7c572a29eb Sat Oct 2 22:06:39 2021 +0100" GCCGO="gccgo" AR="ar" CC="clang" CXX="clang++" CGO_ENABLED="1" GOMOD="/Users/bryan/src/github.com/golang/go/src/go.mod" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" PKG_CONFIG="pkg-config" GOGCCFLAGS="-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/bp/y4j6bp157f52wv55knd_j1fc0000gn/T/go-build3341590964=/tmp/go-build -gno-record-gcc-switches -fno-common"
What did you do?
This program: https://play.golang.org/p/speaQxyFO4a
package main
type fooer interface {
foo()
}
type bar struct{}
func (b *bar) foo() {
}
func f[T fooer](t T) {
t.foo()
}
func main() {
var b bar
f(&b)
}
Compiled with -gcflags "-m -m -l" to show escape analysis.
What did you expect to see?
What I get if t is declared as the concrete type *bar:
/tmp/main.go:12:8: t does not escape
What did you see instead?
/tmp/main.go:12:17: parameter t leaks to {heap} with derefs=0:
/tmp/main.go:12:17: flow: {heap} = t:
/tmp/main.go:12:17: from unsafe.Pointer(t) (interface-converted) at /tmp/main.go:13:3
/tmp/main.go:12:17: from (<node EFACE>).foo() (call parameter) at /tmp/main.go:13:7
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 5
- Comments: 19 (17 by maintainers)
This is getting beyond the scope of this issue, and probably well travelled ground (I haven’t been following any CLs), but this makes me wonder about ubiquitous GC stenciling.
I would agree that we don’t want a special comment to enable full templating (a.k.a. monomorphisation) because then everyone will use it and we could end up just like Rust with bloated compile times and binaries.
However it seems fairly clear to me that there are some situations where the templating overhead is small and the potential gains large. We could make a similar decision as we do when deciding whether to inline functions currently. Specifically, we can consider how to instantiate a function F with some set of type parameters [T₁, T₂, …] with respect to some cost metric C(T₁, T₂, …). A possible cost metric might be as the (estimated) code size of the function’s code multiplied by the total number of instantiations of F (one instantiation for each unique set of type parameters to F). If the cost metric is below some threshold, we fully template the function; otherwise we use GC stenciling, or even full fully generic code (cheaper than reflect but not much) in extreme cases. One canonical “extreme case” is the case of unbounded types (e.g. https://go2goplay.golang.org/p/3kUZ6L8amfd) which would then run OK but be predictably costly)
I’m reworking the top-level optimization phases. I think it’s feasible to devirtualize through known dictionaries.
This is expected. When compiling
f, we don’t know whether the call tofooescapes its receiver or not. Consider adding:The
foofunction ofbazdoes escape its receiver. Butfdoesn’t know if it was instantiated with*baror*baz.Note that we could resolve this if we were fully stenciling. But in our current implementation of generics (with gcshape stenciling and dictionaries) both
f[*bar]andf[*baz]use the same blob of assembly.True, but call-site sensitive escape analysis would help with some generic cases, and actually should help some non-generic cases as well. For example
This function cannot be inlined, so buf is always put on the heap (by the caller, or callers caller or whatever). But actually, it’s safe to put buf on the stack if we know that the Reader doesn’t hold onto it. But I digress.
I mean that the
funcinTestAllocloads the address of a static dictionary.dict.f[*S]and passes that tof[.shape.*uint8]. When the latter is inlined, we can see the callsite is calling a function loaded from a static dictionary entry. We can figure out what target that is.At least, we could figure that out if we can follow the flow of the dictionary after inlining. You’re right that we might have to understand dictionary assignments generated as part of inlining.
Would the approaches discussed in this issue also solve the escape of parameters? Example (I tried to escape the escape by parameterizing on a struct type
FloatHidden, but gc sees through my poor trick):play link
This is an allocation in a hot path that would be nice to avoid, ideally without adding hacks like manual monomorphization (e.g. using code generation). We don’t really need all of the optimizations that inlining could bring (although that would be nice), it would already be enough if the escape property would be propagated upwards.