Description
Background
Currently, interface arguments to functions frequently escape due to subsequent use of the interface in an interface method call.
This affects many APIs, including things like marshalling/unmarshalling APIs, logging APIs, things like fmt.Sprintf, and many other flavors of APIs that take interfaces.
For example, in Go 1.21, the val
here escapes and is heap allocated for this reason (and other for reasons as well):
val := 1000
fmt.Sprintf("%d", val)
If we consider an extremely simplified implementation of Sprintf:
func Print(input any) {
if v, ok := input.(Stringer); ok {
println(v.String())
}
}
When v.String
is called as an interface method in that example, v
might contain a type like Leaking
:
var global any
type Leaking struct {a, b int}
// The receiver l cannot be stack allocated because String leaks l to a global variable.
func (l *Leaking) String() string { global = l; return "" }
The current compiler knows this is possible, and as a result, the input
interface argument to our simple Print function is conservatively marked as escaping. This is part of the reason the real fmt.Sprintf causes val
to be allocated above.
Frequently, though, v
contains a type like Nice
:
type Nice struct {a, b int}
// The receiver n could in theory be stack allocated in n := Nice{}; Print(n)
func (n Nice) String() string { return "something" }
Suggestion
The suggestion is for escape analysis to propagate what it knows about the use of interfaces in method calls to then prove (when it can) whether it is dealing with a type like Leaking or Nice to then avoid allocating the n
in n := Nice{}; Print(n)
.
In particular, escape analysis has had the concept of pseudo locations for things like the heap, and the escape analysis data-flow graph in the current compiler contains an edge leading to a heap pseudo location for each interface method call observed.
We could instead track the interface method use flows separately from other flows to the heap, including propagating across function call boundaries by tagging the associated function & method parameters. When a concrete value is later converted to an interface type (e.g., Print(n)
in our simple example above), we look at what we know about the type and its methods to see if we can prove that it is incapable of leaking if used as a method receiver in an interface method call.
I sent CL 524945 with implementation of the idea, along with some related CLs like 524937 and 524944.
Those are a part of a larger stack targeting the fmt print functions. By the end of my stack (as of CL 528538), this no longer allocates the Point struct on the heap:
type Point struct {x, y int}
func (p Point) X() int { return p.x }
...
p := Point{1, 2}
fmt.Sprintf("%v", p)
(Edit: to better illustrate, also added an example method to Point, which still avoids allocating with CL 528538).
The primary CL 524945 is still WIP, but at least passes all.bash and the TryBots, including passing the more specific interface receiver tests I added. (It passes the older TryBots, but currently fails the new LUCI TryBots for LUCI-specific reasons). The first cut does not differentiate between a type that only has some methods with leaking receivers vs. the specific method in question, but CL 528539 is also a small refinement I had also been thinking about that attempts to address that. (That is not needed for many cases, and that CL is currently in much rougher shape & more exploratory).
In the initial discussion in Gerrit on CL 524945, Matthew Dempsky said he liked the idea. He also suggested opening an issue to help discussion, which is now this issue.
Matthew also later sent CL 526520 with an alternative implementation of the core idea.
Metadata
Metadata
Labels
Type
Projects
Status