-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
GlobalOpt: don't speculatively execute initializers of global variables #30445
Conversation
@swift-ci test |
@swift-ci benchmark |
Performance: -O
Code size: -O
Performance: -Osize
Code size: -Osize
Performance: -Onone
Code size: -swiftlibs
How to read the dataThe tables contain differences in performance which are larger than 8% and differences in code size which are larger than 1%.If you see any unexpected regressions, you should consider fixing the Noise: Sometimes the performance results (not code size!) contain false Hardware Overview
|
@eeckstein sorry if this is confusing but it's important that programmers don't make global initializers dependent on any program side effects. The language semantics that we decided on were pretty clear: global initializers can run any time between the start of the program and the first point of use. If that decision needs to be revisited that should go through the core team |
Build failed |
@eeckstein I can elaborate on the forums, but these are my main points of concern in somewhat arbitrary order:
|
@swift-ci test linux |
@atrick I see this more from a pragmatic view. It's always a win to make swift more predictable, easy to use and safe. When people e.g. develop and test in debug mode and they see a mysterious crash or just a different behavior in the archived product, it does not make their life easier. Regarding performance, we have several language features where we trade performance against safety and predictability: starting from ARC up to runtime exclusivity checking. I doubt that hoisting global inits speculatively will ever be such a significant performance win, compared to what we lose with all this other overhead. Beside all this, the PR is also a code cleanup, because optimizaing global inits is now done by CSE and LICM (where it belongs). A lot of code is removed from GlobalOpt. |
Well, we chose ARC and exclusivity checking knowing that we could optimize them away in important cases, just like we did with the rule about global initializers. I remember hoisting of global initializers being a significant performance win in the Swift 0.x days, but that might've been before we inlined the fast path check on Apple platforms, or had GlobalOpt do optimization of constants into static initializers. If possible, you might want to also run the benchmarks on Linux or Windows, where (unless something's changed) I don't think we assume dispatch_once ABI for global initializers, so global accessors would still pay the full cost of a call on every access. |
Looking at the example from rdar://problem/60292679, it looks like the situation is like this:
and the problem is that we're hoisting the global access out of a loop that never runs, triggering an initialization that would not have happened otherwise. If the loop ran even once, then their code would've crashed with or without optimization. We could argue that hoisting the global access out of the dead loop violates the "initializers run some time before the access" rule, because the access never actually happens in that case. OTOH, if we interpret the rule that way, that makes hoisting much less powerful if we say we need a trip count > 0 to do the hoist. |
@jckarter I am not at all concerned about the overhead of checking for initialization. That falls within the ARC/exclusivity range of safety checks. The issue is optimizing other code within the same loop as a global access. This PR places the requirement on the compiler to analyze all the side effects of the global initializer before performing any optimization that we may want to apply to that loop. The existing model only requires the optimizer to analyze access to an already initialized global. I think we can all strongly agree that the current model has unresolved problems. We can talk about two separate issues
One other thing to note, even with this PR, the crash-on-reentrant-initializer problem still exists! So we'll need to introduce a diagnostic for that one way or another. The only question is whether we consider initialization in unreachable code to be reentrant.
|
I don't think we want to guarantee initializer ordering, beyond the formal dependencies among the initializer expressions themselves. |
@atrick With loops and side effects, we might still be able to take advantage of the fact that the side effects of the initializer happen at most once by doing some loop rotation and/or partial unrolling, if we were able to turn:
into:
That gets more complicated if the access within the loop is also conditional, though. |
@atrick You are right, there are 2 separate issues: Point 1 is what I called "execute an initializer speculatively". Preventing this is not so much of a performance problem for 0-trip count loops, because the compiler already does the loop rotation (what @jckarter mentioned) today. So in most loops we can hoist the initializer call, even if the original loop can have a 0 trip count (except the global is inside a condition in the loop). It's also worth mentioning that all this only affects global with non-trivial types (well, and also enums, but that's another story), because globals with trivial types, e.g. Int, are allocated on the data segment and don't have an initializer anyway. We didn't do that yet in the Swift 0.x days, so I think this is the reason why aggressively hoisting global initializers was very important at that time. |
@eeckstein I agree on all those points, but I'm not sure if your comments still apply to globals defined in another module. We should be able to continue optimizing them when library evolution is disabled, or when they are declared |
I chatted with @atrick about this: it should be no problem. With cross-module optimization initializers can be serialized and therefore analyzed in the calling module. |
…rites Detect stores to globals which get the address from an addressor call (and not from global_addr). This kind of SIL is currently not generated, so most likely this bug does not show up. But as soon as we CSE addressor calls, it can be a real problem.
For example: hoist out of loops where the loop count could be 0. We did this on purpose. But, if not wrong, it's at least very confusing if the initializer has observable side effects. Instead let CSE and LICM do the job and handle initializer side-effects correctly. rdar://problem/60292679
It's needed to hoist global_init calls.
Global initializers are executed only once. Therefore it's possible to hoist such an initializer call to a loop pre-header - in case there are no conflicting side-effects in the loop before the call. Also, the call must post-dominate the loop pre-header. Otherwise it would be executed speculatively.
@swift-ci smoke test |
1 similar comment
@swift-ci smoke test |
Thank you @eeckstein, this change solves actual issues: https://forums.swift.org/t/is-it-possible-to-prevent-inlining-and-tail-call-optimization/21680/11 |
…ized release builds" This reverts commit c62ccf1. swiftlang/swift#30445 is present in the Swift 5.3 release https://github.com/apple/swift/releases/tag/swift-5.3-RELEASE
…ized release builds" This reverts commit c62ccf1 (aimed at fixing #755). We can do this because swiftlang/swift#30445 is present in the Swift 5.3 release https://github.com/apple/swift/releases/tag/swift-5.3-RELEASE
For example: hoist out of loops where the loop count could be 0.
We did this on purpose. But, if not wrong, it's at least very confusing if the initializer has observable side effects.
Instead let CSE and LICM do the job and handle initializer side-effects correctly.
rdar://problem/60292679