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: closure func name changes when inlining #60324
Comments
not only k8s, but for instance leaky go routine tests may fail b/c whitelisting doesn't work correctly anymore. |
Hi Austin, since you are the original author of CL 479095, assigning this bug to you. If you would prefer that I work on this particular cleanup, please assign back to me. Thanks, Than. |
This was noticed before: #55980 |
Is it okay if we just arrange that the |
I don't think I understand the suggestion, but we want the inlining-disabled name to appear in profiles, stack traces, and symbol tables. I don't think changing just runtime.Func will do that. |
For what it's worth, we have the name of the lexically enclosing function at the point of the closure creation, because if the program crashed at that PC we would show that frame in the stack trace marked [inlined]. |
Yes, it's easy to track the original closure symbol name. That's not a problem. What I'm trying to understand is what you want done with that. We currently duplicate the closure and underlying function text, because they can be optimized differently due to different escape analysis and variable capture. So the duplicated closure needs its own linker symbol. |
Does it? I thought the linker addressed all the symbols by table index now, so if we made it a symbol internal to the package being compiled, it would not collide with any others with the same name. |
|
I am interpreting the previous comment as my not having answered your questions, so I will elaborate a bit here. The function names reported in profiles, stack traces, symbol tables, disassembly, and so on should not depend on how aggressively we inline. That is, the names that we get with no inlining at all are the "official" names, and inlining should preserve them. I understand that, in terms of the example at the top, if we do:
that there will be three copies of the actual text symbol generated when f is inlined, and that the texts may actually differ across the 3 calls. But I think the new linker's ability to address symbols by table index instead of name should mean that it's completely fine to have those three text symbols all use the same name: the original name they'd have used without inlining. |
@cherrymui How should the compiler create multiple obj.LSyms that are kept separate but all end up with the same linkname in the symbol tables? |
As background, we originally named closures opaque names like 'func•1', but that was extremely unhelpful in all the places I mentioned where function names appear, not to mention in all tools that didn't support UTF-8. Issue #8291 was to give the closures more useful names, which Dmitri did in CL 3964. Inlining of functions containing closure creation is essentially a regression of that issue. It may seem like they just need a name, any name, but that's not true. The names carry useful information that has been lost since |
Please retract that accusation. The stack trace for your test program is the same in Go 1.19 as in Go 1.20: https://go.dev/play/p/yjP9hrxZ_K0?v=goprev |
It was not an accusation, but I apologize nonetheless. My memory was that one of unified IR's important inlining advances was to enable inlining of closure-containing functions. I misremembered - we started inlining those in Go 1.17, so obviously that must have been with the original IR. In any event, as unified IR has made it possible to do more aggressive inlining, we lose more and more of this information. This is not a criticism of unified IR. My point is only to explain the context of how we got here. All the changes individually make sense, but this is a rough edge where they aren't quite working well enough together. |
Thank you.
Ack. That's what I'm trying to understand: how should this work? As I've said already, we need multiple linker symbols (i.e., obj.LSyms), because in general the inlined closure can get optimized differently than the original closure. You suggest the multiple obj.LSyms should just have identical names in the symbol tables, etc (i.e., use the same linknames). That's not something we do anywhere else in the compiler to my knowledge, so I don't know how to invoke cmd/internal/obj to make that happen. Hence why I asked @cherrymui how to do that. However, I remember she's on vacation, so if any other linker experts can advise on this, that would be great. @golang/compiler |
I think it would be good enough for Go 1.21 if we embed the inlining stack in the name (eliding package names), so for example if F inlines f inlines g inlines h creates a closure, the function would be named F.f.g.h.func1. As long as the same counter is used for all the funcs, it won't matter if that sequences appears again: it would get a different trailing number. So if you had
The four text symbols created in main would be named |
Thanks, that seems doable for 1.21. |
I agree that we should improve the names of inlined closures and the suggestion in #60324 (comment) seems like a reasonable short-term compromise. If that is easy to do at the end of the cycle, great, but I'd push back a bit on this being a release-blocking issue that needs an immediate fix right before the freeze. We know that this is an existing issue that gets more annoying as we are more aggressive with inlining closures, but are we actually that much aggressive than before? CL 479095 states that it increases inlinability by +0.6%. It doesn't say how much it increases inlinability of closures specifically, which is probably the more important metric. But at first glance, it looks like we got unlucky with one specific test rather than us getting sharply more aggressive and causing widespread issues. |
Change https://go.dev/cl/497137 mentions this issue: |
In Go 1.17, cmd/compile gained the ability to inline calls to functions that contain function literals (aka "closures"). This was implemented by duplicating the function literal body and emitting a second LSym, because in general it might be optimized better than the original function literal. However, the second LSym was named simply as any other function literal appearing literally in the enclosing function would be named. E.g., if f has a closure "f.funcX", and f is inlined into g, we would create "g.funcY" (N.B., X and Y need not be the same.). Users then have no idea this function originally came from f. With this CL, the inlined call stack is incorporated into the clone LSym's name: instead of "g.funcY", it's named "g.f.funcY". In the future, it seems desirable to arrange for the clone's name to appear exactly as the original name, so stack traces remain the same as when -l or -d=inlfuncswithclosures are used. But it's unclear whether the linker supports that today, or whether any downstream tooling would be confused by this. Updates #60324. Change-Id: Ifad0ccef7e959e72005beeecdfffd872f63982f8 Reviewed-on: https://go-review.googlesource.com/c/go/+/497137 Reviewed-by: Michael Pratt <mpratt@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Matthew Dempsky <mdempsky@google.com>
Russ's suggested naming change ("main.main.func1" -> "main.main.f.func1") has been implemented and submitted for Go 1.21. I'm happy to implement further changes for Go 1.22, pending advice from the linker devs on how to best approach that. |
Thanks Matthew. |
Do we want the symbols to have the non-unique names? There's a tension: we go to some effort to make runtime tracebacks reflect the language, not the implementation, which suggests those should show the "original" name of the closure, even if that's non-unique; but at the same time developers sometimes need to map that to an ELF/etc symbol, in which case you really want them to be unique (the PC offset traceback prints doesn't do you much good without a unique symbol name!). We could change how we show closures and/or inlining in runtime tracebacks. Is there just a nicer way to present this information? I don't have any answers, but maybe thinking about that bigger picture will inspire someone. |
One possible solution would be to ensure the closure is never optimized differently when inlined, so we can just always reuse the same underlying function all the time. With how things are currently implemented, it's certainly possible that the closure gets optimized differently. But I don't have an example off hand to demonstrate that we do. |
Thanks for the CL @mdempsky. I've confirmed that the fix changes the stack frame in question in the failing Kubernetes test from I've changed this to the Go 1.22 milestone, and I agree with @austin that there is a question about whether we should stop at these not-unique names. It was a release blocker for Go 1.21 because there was no way to test for the presence of a function on a call stack (in, for example, a test of panic behavior) that worked reliably across changes to the amount of inlining. That was breaking real tests with no obvious answer for how to rewrite them to be more robust. Now we have the guarantee that the lexically enclosing function name is preserved. Yes, it was not a new bug in Go 1.21 (as we established, it first started in Go 1.17), but Go 1.21 changes made the problem worse. I appreciate the fix. |
Without inlining, the stack shows
main.f.func1
panicking, which is approximately correct: it's the 1st closure inside main.f.With inlining, the stack instead shows
main.main.func1
, because now it's the 1st closure inside main.main due to inlining of f. However, that's a much less useful name, since the closure is still lexically inside main.f. If there is some way to preserve the old name even when inlining, that would be a good idea.This is the source of a Kubernetes test failure using Go 1.21, because a more complicated function was not being inlined in Go 1.20 but is being inlined in Go 1.21 after CL 492017. The test expected to see "newHandler" on the stack when a closure lexically inside newHandler panicked.
If there's something we can do here, we probably should, both to keep the closure names helpful and to keep tests like the Kubernetes one passing.
The text was updated successfully, but these errors were encountered: