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: escape analysis on interface calls #36964

Open
kirillx opened this issue Feb 1, 2020 · 3 comments
Open

cmd/compile: escape analysis on interface calls #36964

kirillx opened this issue Feb 1, 2020 · 3 comments

Comments

@kirillx
Copy link
Contributor

@kirillx kirillx commented Feb 1, 2020

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

1.13.1

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

on any OS / env

What did you do?

type myCtx struct { data int }

type interface if { Call(ctx *myCtx) }

ctx := myCtx{}
i.Call(&ctx) // i == interface of type 'if'

What did you expect to see?

ctx allocated on stack (as Call() method was empty in my case).

What did you see instead?

ctx allocated on heap as escape analysis doesn't work through interface types. Compiler simply doesn't know the future of the pointer.

Suggestions

As interfaces are widely used in the language it seems quite important to be able to perform such optimisations.
What if interface run-time type info had escape flag for the method. That would allow to generate code for both escaping interface calls and non-escaping ones.

@randall77
Copy link
Contributor

@randall77 randall77 commented Feb 1, 2020

There's lots of issues about resolving interfaces at compile time, so we don't have to allocate their receiver or args: #8618 #18822 #23676 #33160

It's an interesting idea to resolve this at run time instead. Generate code for both cases and decide which allocation path to use.

It can get tricky:

i.Call(&ctx)
j.Call(&ctx)

Then we can only allocate ctx on the stack if both i and j have their Call method marked as not escaping its first argument.

At the time we allocate ctx, we might not know yet which interface methods it might get passed to.

ctx := myCtx{}
... initialize ctx ...
f().Call(&ctx)

Until we can inspect f's return value, we don't know whether ctx escapes.

Sometimes interfaces are wrappers. For example, bufio.NewWriter. When we assign one of those to the io.Writer interface, do we mark its Write method as not escaping its argument? That's a dynamic property of the underlying io.Writer that was passed to NewWriter. How do we associate those things? (We could always punt and be conservative here, but it would be nice to handle.)

@randall77 randall77 added this to the Backlog milestone Feb 1, 2020
@agnivade agnivade changed the title Escape analysis on interface calls cmd/compile: escape analysis on interface calls Feb 2, 2020
@kirillx
Copy link
Contributor Author

@kirillx kirillx commented Mar 5, 2020

More ideas to explore:

  1. Each interface method with name XXX can have a counterpart method XXX_escapes() which would return escape flags and possibly any other performance critical information.
    Thus in the example from randall77 with bufio.NewWriter it would simply return the parameters from the underlying interface and the caller could make a decision whether it needs to allocate the structure or use a stack one.

  2. Instead of getting these performance critical data before calling the method it might be possible to collect it runtime and store in a cache? What if every interface method call would return something we could cache in a map[{interface,method}]perfAndEscapeData ?
    it can be per-goroutine cache, potentially invalidated on GC, but still..

  3. We can instantiate shadow interface instance for a given one with non-nil methods for all unescaping ones. This shadow interface vtable can be filled run-time by methods themselves in their epilogue code when they are sure about their own properties (and all sub-callers are non escaping too).

Ofcause it is quite tricky and 2 and 3 require internal sub interface pointers in bufio.Writer to stay consistent and do not change to anything but nil. Probably it requires a huge logic in the compiler :/

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented May 20, 2020

Here's another data point.

In this program, we have no allocation for the closure or the potentially-escaped variable:

func foo(x int) {
	call(func() int {
		y := &x
		return *y + 2
	})
}

However, in this similar program that uses an interface value, we have an allocation for both the interface and x:

func foo(x int) {
	call(F(func() int {
		y := &x
		return *y + 2
	}))
}

Issue #8618 tracks the issue about allocating storage for the interface. I think this issue is the right one for tracking escape analysis of the value inside the interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants
You can’t perform that action at this time.