Stack or heap, and serialize.go

@mrjn found the allocation in x/serialize.go concerning because some stack allocations might become heap allocations. Here is an example.

Old code:

func f() {
  a := new(MyStruct)
  // Do something to a. Doesn't pass it to anywhere.
}

The variable a is going to be allocated on stack. But if you write

func f() {
  a := NewMyStruct()
  // Do something to a...
}

Then a is going to be allocated on the heap. Initially, I wonder if Go would be smart enough. One thing to point out immediately: If the initialization is light, Go will inline NewMyStruct and there’s no difference. Now, suppose there is no inlining. Let’s verify. Here’s the code.

package main

import (
	"fmt"
)

type S struct{}

func getNew() *S {
	return &S{}
}

func identity(x *S) *S { return x }

func main() {
	a := getNew()
	identity(a)

	var b S
	identity(&b)

	c := new(S)
	identity(c)

	d := new(S)
	fmt.Println(d)

	var e S
	fmt.Println(&e)
}

Then we run go run -gcflags '-l -m' main.go. The -l flag is to disable inlining of identity function. The identity function is to use the variable without using any external function. Here is the output.

./main.go:10: &S literal escapes to heap
./main.go:13: leaking param: x to result ~r1 level=0
./main.go:26: d escapes to heap
./main.go:25: new(S) escapes to heap
./main.go:29: &e escapes to heap
./main.go:29: &e escapes to heap
./main.go:28: moved to heap: e
./main.go:20: main &b does not escape
./main.go:22: main new(S) does not escape
./main.go:26: main ... argument does not escape
./main.go:29: main ... argument does not escape

Indeed, NewStruct will cause heap allocation (if not inlined).

Indeed, local variables (line 20) and new(MyStruct) (line 22) will use the stack.

What I find intriguing is that the variables d and e both go to the heap. An innocent-looking fmt.Println tells Go escape analyzer that d and even e can go elsewhere, and results in using the heap!

Where does a go? Does it not go to heap? Also, is c on stack? Also, is getNew inlined or not?

a,d,e go to heap and b,c go to stack.

After inlining, a goes to stack since there is no difference from c. I verified this by removing the -l flag. The output reads:

./main.go:9: can inline getNew
./main.go:13: can inline identity
./main.go:16: inlining call to getNew
./main.go:17: inlining call to identity
./main.go:20: inlining call to identity
./main.go:23: inlining call to identity
./main.go:10: &S literal escapes to heap
./main.go:13: leaking param: x to result ~r1 level=0
./main.go:26: d escapes to heap
./main.go:25: new(S) escapes to heap
./main.go:29: &e escapes to heap
./main.go:29: &e escapes to heap
./main.go:28: moved to heap: e
./main.go:16: main &S literal does not escape
./main.go:20: main &b does not escape
./main.go:22: main new(S) does not escape
./main.go:26: main ... argument does not escape
./main.go:29: main ... argument does not escape

Line 10 is the &S call. But line 16 is the inlined getNew call and the output reads main &S literal does not escape.

Is there a way to control and ensure that a certain function is always inlined? If we can do that for your serialize code, then it’s a win-win.

I’m afraid there’s no way to force inlining. I am ok with removing the NewMyStruct calls. The main thing I don’t like to see is those flatbuffer offsets. I am perfectly ok with

a := new(MyStruct)
x.ParseMyStruct(a, data)

If you are ok, I can quickly replace those NewMyStruct calls with the above and close that task?

SGTM! And some extra words, to make discuss happy.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.