-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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: inlining confuses escape analysis with interface{} params #53465
Comments
This will be a hard one to fix. The conversion to interface happens when building the Similar to the code below, but for a
The compiler has a |
Could this be fixed as a special case? Recognize exactly these cases: func f {
if safeexpr {
// …
}
} func f {
if !safeexpr {
return
}
// …
} These are very common wrappers for a very common problem with significant readability impact; it might be worth the code weight in the compiler. |
Intuitively, I'm hinting at @josharian's suggestion: as a programmer I'm inclined to move the conditional from the function body to each call site, because there are clearly some gains to be had. But that sort of manual partial inlining is what the compiler should be able to do, not me :)
That does sound like a more generic solution, but I'm not in a position to judge how difficult it is compared to Josh's suggestion. |
Fyi, Zerolog fixed this by not having interface{} parameters in the first place. It does mean the api has to support all types with separate methods. https://github.com/rs/zerolog |
@beoran that is indeed a possible solution, but a drastically different one that I'm not considering at the moment. It is more verbose too, so it doesn't exactly win against Regardless, I think it would be best for the Go compiler to gain this optimization, because |
Here's my inserting of the |
I think this is the same as I see in github.com/go-log/logr (xref #56873). I am seeing people adopt unnatural caller-side checking which feels like what mvdan said (#53465 (comment))
|
Should the basic problem get addressed, would it also eliminate function calls that are needed to construct the parameter slice? In Kubernetes, we often have:
where |
@pohly this issue is just for the escape analysis. That is, if the logger is disabled, the log arguments should not be escaping. A few allocations per log call add up. For the work involved in preparing the logged values, see #37739. For pure functions maybe the compiler could be smart enough to do this for you, but I'd follow that issue regardless. |
Perhaps interesting to @dr2chase given his recent work on better inlining for iterator funcs, e.g. #69411. I still think something like #53465 (comment) would be worthwhile. |
FWIW, it seems to me this might be solved by #8618 (comment) and #62653. (However, I find escape analysis to be subtle, so I probably should have thought longer about this example before speaking here 😅). Using one the CLs from that stack, the ConditionalDebugf/false case goes to 0 allocations (which if I understood seems to be @mvdan's main goal here). Also, the ConditionalDebugf/true case with that same CL goes from 3 allocations to 1 allocation. (The heap allocation of the Sprintf string result remains, which might be required/expected given the Sprintf result is assigned to a global by @mvdan's benchmark).
(That particular CL 528538 is abandoned mainly because it relied on me having created a new pragma to solve a reflect.Value.Interface allocation in an earlier CL, but I later concluded a more general approach was likely possible, which is now spread across a few other CLs in that stack. #8618 (comment) has a brief overview of that). All that said, changes along the lines suggested by Josh above or Keith above could also be useful in their own right, or maybe there is a good inining change to make here... |
Interesting, thank you. When your changes land in master I'll try again with my real project to see whether the allocations there are still present. The example in the original post here is a simplification, so the funcs involved are a lot smaller. |
Take a look at https://go.dev/play/p/mPqA0RaybuQ.
On
go version devel go1.19-9068c6844d Thu Jun 16 21:58:40 2022 +0000 linux/amd64
I get the following benchmark results:First, note that both debugf functions are inlined:
And, in the
true
sub-benchmarks where debug logging is enabled and we actually callSprintf
, we can see three allocations - two corresponding to the twointerface{}
parameters, as thestring
andint
variables escape to the heap via the interface, and one for the actual call toSprintf
constructing a string.So far so good. However, when debug logging is disabled, I want to avoid those allocations, because this debug logging happens in a tight loop that runs millions of times in a program, so I want to avoid millions of allocations made in a very short amount of time.
This works fine for the "manually inlined" case where I wrap the call to Debugf with a conditional. Note the zero allocations on
InlinedDebugf/false
. However, that means that every time I want to use Debugf (a dozen times in said program), I need three lines rather than one, and it's pretty repetitive and verbose.One good alternative is to move the conditional inside the Debugf function - and since it's still inlined, it should be the same result. However,
ConditionalDebugf/false
allocates twice, presumably because the two arguments do still allocate, as if I was actually callingSprintf
when in fact I am not.It seems to me like the two benchmarks should behave the same; the compiler's escape analysis and inliner should work well enough together to detect this scenario.
Note that my real use case involves
log.Printf
, which is a no-op when one has calledlog.SetOutput(io.Discard)
. The benchmark above is a simplification which still reproduces the same problem.The text was updated successfully, but these errors were encountered: