-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cmd/compile: generic function argument causes escape to heap #48849
Comments
This is expected. When compiling
The Note that we could resolve this if we were fully stenciling. But in our current implementation of generics (with gcshape stenciling and dictionaries) both |
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) |
Thank you for the explanation, and the follow-up. FWIW the reason I tried this was the regexp package has three input variants: byte slice, string and io.Reader, so the expected number of instantiations is small and the expected benefit from inlining, not-escaping, etc., is relatively high. |
This example at least seems solvable with improved escape analysis. Right now |
That's not necessarily true. The caller might have |
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. |
Change https://golang.org/cl/360015 mentions this issue: |
There was no way to use an interface because the methods on the Point types return concrete Point values, as they should. A couple somewhat minor annoyances: - Allocations went up due to #48849. This is fine here, where math/big causes allocations anyway, but would probably not be fine in nistec itself. - Carrying the newPoint/newGenerator functions around as a field is a little weird, even if type-safe. It also means we have to make what were functions methods so they can access newPoint to return the zero value. This is #35966. For #52182 Change-Id: I050f3a27f15d3f189818da80da9de0cba0548931 Reviewed-on: https://go-review.googlesource.com/c/go/+/360015 Reviewed-by: Ian Lance Taylor <iant@google.com> Run-TryBot: Filippo Valsorda <filippo@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Roland Shoemaker <roland@golang.org> Reviewed-by: Russ Cox <rsc@golang.org>
Moving to 1.20 milestone. |
In triage, @randall77 notes that PGO information might make this much easier. No plans on doing this in the near future, for now. |
I encountered this and I was surprised by the behavior, mainly because I didn't have a similar problem with interfaces. I'm probably missing something: package main
import (
"fmt"
"testing"
)
type S struct {
Val int
}
type I interface {
SetVal(v int)
}
func (s *S) SetVal(v int) {
s.Val = v
}
func f[T I](s T) {
s.SetVal(5)
}
func TestAlloc(t *testing.T) {
fmt.Println(testing.AllocsPerRun(1, func() {
var s S
f(&s)
}))
} This allocates:
package main
import (
"fmt"
"testing"
)
type S struct {
Val int
}
type I interface {
SetVal(v int)
}
func (s *S) SetVal(v int) {
s.Val = v
}
func f(s I) {
s.SetVal(5)
}
func TestAlloc(t *testing.T) {
fmt.Println(testing.AllocsPerRun(1, func() {
var s S
f(&s)
}))
} This does not alloc:
I would expect the two cases to work more or less the same. |
@CannibalVox I'm not entirely sure why the inline/devirtualize optimization doesn't apply to the generic code. I suspect there's a phase-ordering issue, or the shapifying is somehow blocking the devirtualization step. Maybe someone else knows? |
@randall77 method expression on type parameter ( If type parameter is instantiated with an interface type, maybe we should return the method expression directly, instead of the indirect call. |
So if
This isn't instantiating with an interface type, it is instantiating with |
Ah right.
What "constant" do you mean in "constant dictionary"? Does it mean the I'm not sure we have enough information to unpack the dictionary when devirtualization happens. |
I mean that the 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 package main
func main() {
{
ts := TimeSeries[*FloatOf]{pending: new(FloatOf)}
f := FloatOf(32) // Escape (due to Add).
ts.Add(&f)
}
{
ts := TimeSeries[FloatHidden]{pending: NewFloatHidden(0)}
f := NewFloatHidden(94) // Escape (due to Add).
ts.Add(f)
}
{
ts := &TimeSeriesFloat{pending: new(FloatOf)}
f := FloatOf(64) // No escape.
ts.Add(&f)
}
}
type Observable[T any] interface{ Add(other T) }
// ==== TimeSeries[T] and the manually monomorphized variant: TimeSeriesFloat ====
type TimeSeries[T Observable[T]] struct{ pending T }
func (ts *TimeSeries[T]) Add(observation T) { ts.pending.Add(observation) }
type TimeSeriesFloat struct{ pending *FloatOf }
func (ts *TimeSeriesFloat) Add(observation *FloatOf) { ts.pending.Add(observation) }
// ==== Types that implement Observable[T] ====
type FloatOf float64
func (f *FloatOf) Add(other *FloatOf) { *f += *other }
// FloatHidden is an attempt to work around
// https://planetscale.com/blog/generics-can-make-your-go-code-slower by not
// parameterizing on a pointer type. Sadly, it doesn't work.
type FloatHidden struct{ Ptr *float64 }
func NewFloatHidden(f float64) FloatHidden { return FloatHidden{Ptr: &f} }
func (f FloatHidden) Add(other FloatHidden) { *f.Ptr += *other.Ptr } 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. |
Hi @aktau, your example seems to crash with a nil pointer dereference: https://go.dev/play/p/JQj8ERexpjb. FWIW, I have a large-ish WIP stack of CLs that attempt some escape analysis improvements, and at first glance, I thought it might help at least part of your example, but at second glance, I'm less sure, including I might have confused myself as to which pieces in your example are your desired code vs. which pieces are attempted workarounds. I also have a general question-- are there some places in your example you could just use floats instead of pointers to floats? (The more pointers you have, the harder it is for escape analysis of course). Also, escape analysis usually doesn't like a pointer dereference on the LHS of an assignment, which you might be doing. Sorry, probably not a very helpful comment based just on a quick look. |
@thepudds sorry about that. I copy/pasted a simplified version of my code without checking that it actually runs. I had only checked the
In this case, floats are just an example of an observable. In real code, this could be some aggregate data structure. E.g.:
Making the parameter a non-pointer type would complicate the implementation of the data structure, and also pass a potentially very large struct through registers and on the stack. The receiver must in any case be a pointer.
This is the reason why I included the monomorphized non-generic variant of the timeseries data type: |
I'm reworking the top-level optimization phases. I think it's feasible to devirtualize through known dictionaries. |
I ran into what I think is a form of this today, so I'll just submit it as a use-case and a +1 for any potential improvement here. There appears to be no way to return a type https://godbolt.org/z/3f9bWrcrs type X struct{}
func (x *X) Unmarshal([]byte) {}
type Unmarshaler[T any] interface {
Unmarshal([]byte)
*T
}
// out escapes
func UnmarshalNew[T any, U Unmarshaler[T]](p []byte) T {
var out U = new(T)
out.Unmarshal(p)
return *out
}
// out escapes
func UnmarshalConversion[T any, U Unmarshaler[T]](p []byte) T {
var out T
U(&out).Unmarshal(p)
return out
}
// fully inlines
func DirectUnmarshalNew(p []byte) X {
var out *X = new(X)
out.Unmarshal(p)
return *out
}
// fully inlines
func DirectUnmarshalConversion(p []byte) X {
var out X
out.Unmarshal(p)
return out
} |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes, needs generics.
What operating system and processor architecture are you using (
go env
)?go env
OutputWhat did you do?
This program: https://play.golang.org/p/speaQxyFO4a
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
:What did you see instead?
The text was updated successfully, but these errors were encountered: