Thanks Robert, I've come to the same conclusion. I'm guessing the approach simplifies and speeds up the escape analysis, though appears to be a significant limitation.
The following extract from escape.go seems to apply. https://tip.golang.org/src/cmd/compile/internal/escape/escape.go // Every Go language construct is lowered into this representation, // generally without sensitivity to flow, path, or context; and // *without distinguishing elements within a compound variable*. For // example: // // var x struct { f, g *int } // var u []*int // // x.f = u[0] // // is modeled simply as // // x = *u // // That is, we don't distinguish x.f from x.g, or u[0] from u[1], // u[2], etc. However, we do record the implicit dereference involved // in indexing a slice. Thanks for your help on this. Cheers, Rik On Saturday, August 31, 2024 at 10:45:46 AM UTC+12 Robert Engels wrote: > Yes, but the called function can retain a reference to the string - > strings are pointers. it cannot retain a reference to the scalar. > > I’m guessing if any of the elements can be retained then the entire > structure is allocated on the heap - I don’t think it has to but it is > probably the current implementation. > > On Aug 30, 2024, at 5:34 PM, rkerno <rik.ke...@projectcatalysts.com> > wrote: > > Thanks Robert. I don't think it's the value that's causing this....I > still get the allocation if I remove the value from the equation. It's > something to do with the root level having children. > > > The escape analysis from the refactored code below is: > go build -gcflags='-m=2' . > ./alloc.go:12:27: parameter y leaks to {heap} with derefs=0: > ./alloc.go:12:27: flow: {heap} = y: > ./alloc.go:12:27: from y.key (dot) at ./alloc.go:19:6 > > *./alloc.go:12:27: from fn(y.key) (call parameter) at ./alloc.go:19:4* > > Strings can't reference anything, AFAIK. > > BenchmarkAllocations0-2 97209274 28.76 ns/op > 0 B/op 0 allocs/op > BenchmarkAllocations1-2 1395814 796.9 ns/op > 1152 B/op 1 allocs/op > > type ( > Y struct { > key string > children []Y > unused [128]uint64 // Demonstrates that the whole struct is placed on > the heap! > } > useKeyFunc = func(key string) > ) > > func scanY(fn useKeyFunc, y Y) { > if y.children != nil { > for i := 0; i < len(y.children); i++ { > scanY(fn, y.children[i]) > } > return > } > fn(y.key) > } > > func printKey(key string) { > } > > func Benchmark_ZeroAllocations(b *testing.B) { > b.ReportAllocs() > b.ResetTimer() > b.RunParallel(func(pb *testing.PB) { > for pb.Next() { > scanY( > printKey, > Y{ > key: "root", > children: []Y{}, > }, > ) > } > }) > } > > func Benchmark_OneAllocation(b *testing.B) { > b.ReportAllocs() > b.ResetTimer() > b.RunParallel(func(pb *testing.PB) { > for pb.Next() { > scanY( > printKey, > Y{ > key: "root", > children: []Y{ > { > key: "level1", > children: nil, > }, > }, > }, > ) > } > }) > } > > On Saturday, August 31, 2024 at 5:02:28 AM UTC+12 robert engels wrote: > >> because when you only access an int that is passed by value in the >> function it knows it can’t escape. >> >> if you pass the string/value than the the variable can escape, and since >> the value could point back to the struct itself, it can escape, meaning the >> entire struct needs to be on the heap. >> >> >> On Aug 30, 2024, at 3:43 AM, rkerno <rik.ke...@projectcatalysts.com> >> wrote: >> >> Hi Everyone, >> >> https://gist.github.com/rkerno/c875609bdeb2459582609da36b54bf72 >> >> I'm struggling to wrap my head around the root cause of this issue. >> >> Accessing certain fields of a struct triggers go to allocate the complete >> struct on the heap. The key ingredients are: >> >> - A struct with a contained slice of structs defined inline >> - Calls to functions via a function pointer >> >> What follows is a contrived example to demonstrate / investigate the >> problem. >> >> const ( >> // >> // When requireZeroAllocations == true... >> // >> // BenchmarkAllocations-2 27763290 52.28 >> ns/op 0 B/op 0 allocs/op >> // >> // When requireZeroAllocations == false... >> // >> // BenchmarkAllocations-2 14403922 1532.00 >> ns/op 2304 B/op 2 allocs/op >> // >> requireZeroAllocations = false >> ) >> >> type ( >> X struct { >> id uint >> key string >> value any >> children []X >> useId useIdFunc >> useX useXFunc >> useKey useKeyFunc >> useValue useValueFunc >> unused [128]uint64 // Demonstrates that the whole struct is placed on >> the heap! >> } >> useIdFunc = func(id uint) >> useXFunc = func(key string, value any) >> useKeyFunc = func(key string) >> useValueFunc = func(value any) >> ) >> >> func scanX(x X) { >> if len(x.children) > 0 { >> for i := 0; i < len(x.children); i++ { >> scanX(x.children[i]) >> } >> return >> } >> >> x.useId(x.id) >> if !requireZeroAllocations { >> // Why can we access the id field without an allocation, but as soon as >> we access >> // the string or any fields the entire struct is placed on the heap? >> x.useKey(x.key) >> x.useX(x.key, x.value) >> x.useValue(x.value) >> } >> } >> >> func printId(id uint) { >> } >> func printX(key string, value any) { >> } >> func printKey(key string) { >> } >> func printValue(value any) { >> } >> >> >> The benchmark used to demonstrate the behaviour is: >> >> func BenchmarkAllocations(b *testing.B) { >> b.ReportAllocs() >> b.ResetTimer() >> b.RunParallel(func(pb *testing.PB) { >> for pb.Next() { >> scanX( >> X{ >> id: 0, >> key: "root", >> value: nil, >> children: []X{ >> { >> id: 1, >> key: "level1", >> value: nil, >> children: []X{ >> { >> id: 2, >> key: "k1", >> value: "v1", >> children: nil, >> useId: printId, >> useX: printX, >> useKey: printKey, >> useValue: printValue, >> }, >> }, >> }, >> }, >> }, >> ) >> } >> }) >> } >> >> Any insights or ideas why go exhibits this behaviour would be appreciated. >> >> Cheers, >> Rik >> >> -- >> You received this message because you are subscribed to the Google Groups >> "golang-nuts" group. >> To unsubscribe from this group and stop receiving emails from it, send an >> email to golang-nuts...@googlegroups.com. >> To view this discussion on the web visit >> https://groups.google.com/d/msgid/golang-nuts/3ff0270c-caf0-4a23-9cdf-08bf40e83a33n%40googlegroups.com >> >> <https://groups.google.com/d/msgid/golang-nuts/3ff0270c-caf0-4a23-9cdf-08bf40e83a33n%40googlegroups.com?utm_medium=email&utm_source=footer> >> . >> >> >> -- > You received this message because you are subscribed to the Google Groups > "golang-nuts" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to golang-nuts...@googlegroups.com. > > To view this discussion on the web visit > https://groups.google.com/d/msgid/golang-nuts/8e726419-e984-4873-a2e9-d6a6eb4794a3n%40googlegroups.com > > <https://groups.google.com/d/msgid/golang-nuts/8e726419-e984-4873-a2e9-d6a6eb4794a3n%40googlegroups.com?utm_medium=email&utm_source=footer> > . > > -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/6e453470-1d5e-47c1-97e7-67e7948d9c7fn%40googlegroups.com.