-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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: teach escape analysis to conditionally stack alloc interface method call parameters like a slice in w.Write(b) #72036
Comments
We've been calling this "dynamic escapes". @cherrymui has done some experiments in this area. I'm not sure if we have an existing issue filed, I couldn't find one. cc @golang/compiler |
Some possible extensions:
Or, maybe not. 😅 |
Thanks @prattmic. Hi @cherrymui, I'll be curious to hear your thoughts, especially if you've done some experiments. (FWIW, I've been making progress on #62653 and #8618 (comment), though I switched back to this because (1) this actually has fewer moving parts and (2) I'm currently planning or at least hoping to use some of the machinery from here back on #62653). |
Go version
tip
What did you do?
Run this benchmark:
What did you see happen?
Interface method arguments are marked as escaping. In our example, the byte slice is heap allocated due to its use as the argument to the
w.Write
:What did you expect to see?
It would be nice to see 0 allocations.
Suggestion
I suspect in many cases we can teach escape analysis to only conditionally heap allocate an interface method parameter (the byte slice
b
in our example) depending on what concrete implementation is passed in at execution time behind the interface.Today, escape analysis cannot generally reach different conclusions for distinct control flow paths, nor set up a single value to be conditionally stack or heap allocated, but we could teach it to do those things in certain situations, including for the type assertions that are automatically introduced at compile time by PGO-based devirtualization. The suggested approach doesn't solely rely on PGO, but people who are concerned about performance really should be using PGO, including PGO has some inlining superpowers that are helpful for avoiding allocations. (Today, PGO devirtualization doesn't avoid the allocation we are discussing).
This is to help target cases where the concrete type behind an interface is not statically known. (That is, the compiler today is able to recognize in cases like
w := &GoodWriter{}; w.Write(b)
that staticallyw
must have a specific concrete type, but that does not help today when the concrete type forw
is not statically known, such as ifw
is passed in as a parameter as in our example above).I have a basic working version, and I plan to send a CL. It still needs cleanup, more tests, I need to look at a broader set of results, etc., and it has a couple of shortcuts that I am in the middle of removing, but I am cautiously hopeful it can be made to work robustly.
Benchmark results for the example from the playground link above using that CL with PGO enabled:
Any feedback welcome, especially of the flavor "This will never work for reasons X and Y" (which would save everyone time 😅).
Discussion
When compiling
CallWrite
, the compiler does not know the concrete implementation hiding behind interfacew
. For example,CallWrite
might be invoked with a nice concrete implementation that does not retain/leak thew.Write
byte slice argument (likeGoodWriter
in our example above), orCallWrite
might be invoked with something likeLeakingWriter
:io.Writer
happens to document that implementations must not retain the byte slice, but the compiler doesn't read documentation and this type of allocation also affects other interfaces as well that don't document similar prohibitions.If we collect a profile that is able to observe that
CallWrite
is frequently called withGoodWriter
, the compiler can use PGO to conditionally devirtualizew
in most cases, and effectively rewrite the IR at compile time to something akin to:However, the byte slice still must be heap allocated given the function can be called with something other than
GoodWriter
.We might be tempted to help things by manually adding a second type assertion around the
make
:However, that still heap allocates with today's escape analysis, which constructs a directed data-flow graph that is insensitive to control flow branches.
WIP CL
Our WIP CL teaches escape analysis to recognize type assertions like those created by PGO devirtualization (or by a human), track what happens in the concrete call case (e.g., does the concrete call also cause
b
to escape), propagate the interface method argument use back to locations like themake
in our original example, and if warranted, rewrite a single potentially allocating location (like a singlemake
) to have instead have two locations protected by appropriate type assertions in the IR.The net result is the slice backing array in one location has been proven to be safe to place on the stack and while the other location is heap allocated (and which one of those happens depends on what interface is passed in at execution time).
In other words, a human can write the original three-line
CallWrite
from above, and the compiler can conclude it is worthwhile to transform it to theTwoTypeAsserts
form and do the work to make the heap allocation conditional.In the WIP implementation, some current limitations include:
w
in our example) must currently be passed in as a function parameter. I think this restriction could be relaxed, though a tricky case is ifw
is a field on another struct. (I'll briefly comment more on that below).w
) must not be reassigned in view of the allocating location or interface method call. This might be a reasonable restriction for a first cut. (OTOH, the potentially allocating value can be reassigned, as shown in our examples above).The text was updated successfully, but these errors were encountered: