Skip to content
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: argument escapes when it should not #18001

Closed
cherrymui opened this issue Nov 21, 2016 · 6 comments
Closed

cmd/compile: argument escapes when it should not #18001

cherrymui opened this issue Nov 21, 2016 · 6 comments
Labels
FrozenDueToAge NeedsFix The path to resolution is known, but the work has not been done.
Milestone

Comments

@cherrymui
Copy link
Member

What version of Go are you using (go version)?

tip (0bae74e)

Compile the following code:

func f() {
        var t int
        g(&t)
}

func h(x int) {
        g(&x)
}

func h2() (x int) {
        g(&x)
        return
}

//go:noinline
func g(*int) {}

I think all of the three functions, f, h, h2, should not allocate on heap. But it actually does for all three. For example, f:

	0x0000 00000 (esc.go:3)	TEXT	"".f(SB), $24-0
	0x0000 00000 (esc.go:3)	MOVQ	(TLS), CX
	0x0009 00009 (esc.go:3)	CMPQ	SP, 16(CX)
	0x000d 00013 (esc.go:3)	JLS	76
	0x000f 00015 (esc.go:3)	SUBQ	$24, SP
	0x0013 00019 (esc.go:3)	MOVQ	BP, 16(SP)
	0x0018 00024 (esc.go:3)	LEAQ	16(SP), BP
	0x001d 00029 (esc.go:3)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (esc.go:3)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (esc.go:4)	LEAQ	type.int(SB), AX
	0x0024 00036 (esc.go:4)	MOVQ	AX, (SP)
	0x0028 00040 (esc.go:4)	PCDATA	$0, $0
	0x0028 00040 (esc.go:4)	CALL	runtime.newobject(SB)
	0x002d 00045 (esc.go:4)	MOVQ	8(SP), AX
	0x0032 00050 (esc.go:4)	MOVQ	$0, (AX)
	0x0039 00057 (esc.go:5)	MOVQ	AX, (SP)
	0x003d 00061 (esc.go:5)	PCDATA	$0, $0
	0x003d 00061 (esc.go:5)	CALL	"".g(SB)
	0x0042 00066 (esc.go:6)	MOVQ	16(SP), BP
	0x0047 00071 (esc.go:6)	ADDQ	$24, SP
	0x004b 00075 (esc.go:6)	RET
	0x004c 00076 (esc.go:6)	NOP
	0x004c 00076 (esc.go:3)	PCDATA	$0, $-1
	0x004c 00076 (esc.go:3)	CALL	runtime.morestack_noctxt(SB)
	0x0051 00081 (esc.go:3)	JMP	0

Escape analysis debug output shows

$ go tool compile -m -m esc.go 
esc.go:18: cannot inline g: marked go:noinline
esc.go:3: cannot inline f: non-leaf function
esc.go:8: cannot inline h: non-leaf function
esc.go:12: cannot inline h2: non-leaf function
esc.go:5: &t escapes to heap
esc.go:5: 	from &t (passed to call[argument escapes]) at esc.go:5
esc.go:4: moved to heap: t
esc.go:9: &x escapes to heap
esc.go:9: 	from &x (passed to call[argument escapes]) at esc.go:9
esc.go:8: moved to heap: x
esc.go:13: &x escapes to heap
esc.go:13: 	from &x (passed to call[argument escapes]) at esc.go:13
esc.go:12: moved to heap: x

@dr2chase

@JayNakrani
Copy link
Contributor

JayNakrani commented Nov 22, 2016

For this case, it certainly doesn't escape. But what if some child function of g() uses the variable asynchronously. Consider this case,

func f() {
  var t int
  g(&t)
  // f() can return before goroutine inside g() finishes. 
}

//go:noinline
func g(a *int) {
  go fn(b *int) {
   // some thing here that takes enough time for f() to return first
    _ = b
  }(a)
}

How can we determine at compile time that all the functions in call-graph rooted at g(), do not access t after f() has returned?

@cherrymui
Copy link
Member Author

Passing to other (or new) goroutines makes it escape.

@JayNakrani
Copy link
Contributor

JayNakrani commented Nov 22, 2016

Yes, but how do you determine that at compile time?

I am not sure if we can do this statically though. Consider following case: Whether the var escapes or not can not be determined until run time.

type doer interface{
  func do(i *int)
}

type syncDoer struct {}

func (syncDoer) do(i *int) {
  // accesses i synchronously
}

type asyncDoer struct {}

func (asyncDoer) do(i *int) {
  // accesses i asynchronously
  go func(b *int) {
    // time consuming work
    _ = b
  }(i)
}

func f(s doer) {
  var t int
  g(s, &t)
  // f() can return before goroutine inside g() finishes. 
}

//go:noinline
func g(s doer, a *int) {
  s.do(a)
}

func haha() {
  if ( ...some run-time condition... ) {
    f(syncDoer{})
  } else {
    f(asyncDoer{})
  }
}

@quentinmit quentinmit added the NeedsFix The path to resolution is known, but the work has not been done. label Nov 22, 2016
@quentinmit quentinmit added this to the Go1.9 milestone Nov 22, 2016
@quentinmit
Copy link
Contributor

Probably too late for 1.8 unless the fix is trivial.

@cherrymui
Copy link
Member Author

@Dhananjay92, passing object to function pointer (including interface) should also make it escape.

@cespare
Copy link
Contributor

cespare commented Apr 6, 2017

This is all due to the fact that the parameter to g doesn't have a name. This was fixed in CL 38600. On tip, everything's good:

test.go:18:6: cannot inline g: marked go:noinline
test.go:3:6: cannot inline f: non-leaf function
test.go:8:6: cannot inline h: non-leaf function
test.go:12:6: cannot inline h2: non-leaf function
test.go:20:6: can inline main as: func() {  }
test.go:5:4: f &t does not escape
test.go:9:4: h &x does not escape
test.go:13:4: h2 &x does not escape

@cespare cespare closed this as completed Apr 6, 2017
@golang golang locked and limited conversation to collaborators Apr 6, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsFix The path to resolution is known, but the work has not been done.
Projects
None yet
Development

No branches or pull requests

5 participants