-
Notifications
You must be signed in to change notification settings - Fork 10.6k
SILOptimizer: don't remove empty conflicting access scopes #85533
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
Conversation
|
@swift-ci smoke test |
|
@swift-ci apple silicon benchmark |
|
@swift-ci smoke test windows |
|
Dead code elimination should definitely be able to delete empty accesses. From the test case, without seeing the SIL, it looks like an RLE bug. RLE can't reuse a load from a conflicting access scope without creating a dependency on the begin_access. |
a1460cf to
07a8875
Compare
For RLE this would work. However I found that for dead-store elimination this would not work, because it would need to consider potential conflicts in called functions, i.e. it's not a function-local problem. Therefore I decided to go another way. Instead of adding logic to RLE and DSE (and potentially other optimizations) I removed the simple unconditional dead-access-scope eliminations and added a new pass which can remove dead scopes - by considering conflicts. |
|
@swift-ci test |
|
@swift-ci apple silicon benchmark |
07a8875 to
bf694e0
Compare
|
@swift-ci test |
|
@swift-ci apple silicon benchmark |
Empty access scopes can be a result of e.g. redundant-load-elimination. It's still important to keep those access scopes to detect access violations. Even if the load is physically not done anymore, in case of a conflicting access a propagated load is still wrong and must be detected. rdar://164571252
Empty access scopes can be a result of e.g. redundant-load-elimination. It's still important to keep those access scopes to detect access violations. Even if the load is physically not done anymore, in case of a conflicting access a propagated load is still wrong and must be detected. rdar://164571252
It checks if arbitrary functions may be called by an instruction. This can be either directly, e.g. by an `apply` instruction, or indirectly by destroying a value which might have a deinitializer which can call functions.
It is a set which supports iterating over its elements.
It is like `Worklist` but can store an additional arbitrary payload per element.
It eliminates dead access scopes if they are not conflicting with other scopes. Removes: ``` %2 = begin_access [modify] [dynamic] %1 ... // no uses of %2 end_access %2 ``` However, dead _conflicting_ access scopes are not removed. If a conflicting scope becomes dead because an optimization e.g. removed a load, it is still important to get an access violation at runtime. Even a propagated value of a redundant load from a conflicting scope is undefined. ``` %2 = begin_access [modify] [dynamic] %1 store %x to %2 %3 = begin_access [read] [dynamic] %1 // conflicting with %2! %y = load %3 end_access %3 end_access %2 use(%y) ``` After redundant-load-elimination: ``` %2 = begin_access [modify] [dynamic] %1 store %x to %2 %3 = begin_access [read] [dynamic] %1 // now dead, but still conflicting with %2 end_access %3 end_access %2 use(%x) // propagated from the store, but undefined here! ``` In this case the scope `%3` is not removed because it's important to get an access violation error at runtime before the undefined value `%x` is used. This pass considers potential conflicting access scopes in called functions. But it does not consider potential conflicting access in callers (because it can't!). However, optimizations, like redundant-load-elimination, can only do such transformations if the outer access scope is within the function, e.g. ``` bb0(%0 : $*T): // an inout from a conflicting scope in the caller store %x to %0 %3 = begin_access [read] [dynamic] %1 %y = load %3 // cannot be propagated because it cannot be proved that %1 is the same address as %0 end_access %3 ``` All those checks are only done for dynamic access scopes, because they matter for runtime exclusivity checking. Dead static scopes are removed unconditionally.
bf694e0 to
9a12474
Compare
|
@swift-ci test |
|
@swift-ci apple silicon benchmark |
I'm not sure why removing a dead store would require a conflicting access to be preserved. If the stored value is never used, then there's no real conflict. It's good for optimization to eliminate conflicting accesses if the result of the conflicting access is not observable. |
This can happen if the dead store is in an enclosing scope of a conflicting inner scope. Even with the "fix" in RLE, this would be the case in the |
For the record, until now, we always expected that the optimizer would remove such truly dead access scopes, regardless of whether they conflict with another access. I don't have an example on hand that shows how important it is to remove dead access scopes, but I know runtime exclusivity checks are very often the performance bottleneck as we get better at avoiding retain/release. The exclusivity runtime traps are also very difficult to analyze in release builds; we'd rather not trigger them at -O unless there is a real observable conflict (that could cause the program to misbehave). Consider load elimination. Dead loads can simply be removed along with their access scope. Dead stores, like dead loads, can simply be removed along with their access scope because the stored value is never observered. Redundant loads are different because the program observes the value as if the memory were accessed at the point of the original load. So, removing a redundant load creates a logical dependency on its access scope. We really should be representing that dependency in SIL. I agree that removing dead access scopes means we need to be careful in any optimization that removes redundant loads, like mem2reg, RLE, and maybe others. So that introduces some burden on the optimizer. But, again, the dependency on the access scope is real, so it's cheating to remove the load without a mark_dependence. In the "copyable" test case, this line semantically copies the value in
|
Originally I wanted to do that. However that would mean that many followup optimizations need to correctly maintain this mark_dependence. This would create endless complexity in the optimizer. For example, If the value of a removed load is a stored constant, the constant propagation pass would need to "move" the mark_dependence to the instruction where the constant is propagated to. Even worse, bugs in such dependence updating logic wouldn't be found (until someone notices a missing runtime error for wrong code by chance). Compared to that, DeadAccessScopeElimination is a simple and robust solution which removes the burden of many optimization passes to handle access scope dependencies.
Forgot to mention that the problem with DCE also shows up in the non-copyable test case. |
I have no problem with a separate pass. I just want to make it clear that it's legal to remove dead conflicting scopes but we're choosing not to do that (yet) to avoid representing the dependency created by redundant loads. |
It's not that simple. We have a similar problem with dead store elimination (and potentially other optimizations). after inlining everything and de-virtualizing Let's assume we even don't run RLE, so we don't remove the redundant load. |
Thanks for that example. This a different kind of problem. I was only considering the transformation that creates a dead access scope. But here, the invalid optimization has already happened before the access is dead, probably based on incorrect alias information. We need to preserve the conflicting access to catch that incorrect assumption about aliasing. So, I agree with your strategy. We want optimization to benefit from assuming that accesses cannot conflict. But since we can't easily represent all those assumptions, we need to enforce all access conflicts just in case. |
What kind of invalid optimization do you mean? This is more or less the SIL after inlining and release-devirtualization. No other relevant optimization did run to produce this SIL. |
The original code is not First someone must have removed the load of The aliasing problem I was thinking of would be if the load of At any rate, if the SIL you posted is correct out of SILGen, then it would be legal to delete the store and its access scope because the stored value is not observable. |
Empty access scopes can be a result of e.g. redundant-load-elimination. It is important to keep such access scopes (if they might be conflicting) to detect access violations at runtime.
This PR removes the simple unconditional dead access scope elimination optimizations from instruction simplification and dead-code elimination.
To compensate for that, I added new DeadAccessScopeElimination pass. For example, it removes:
However, dead conflicting access scopes are not removed.
If a conflicting scope becomes dead because an optimization e.g. removed a load, it is still important to get an access violation at runtime.
Even a propagated value of a redundant load from a conflicting scope is undefined.
After redundant-load-elimination:
In this case the scope
%3is not removed because it's important to get an access violation error at runtime before the undefined value%xis used.This pass considers potential conflicting access scopes in called functions.
But it does not consider potential conflicting access in callers (because it can't!).
However, optimizations, like redundant-load-elimination, can only do such transformations if the outer access scope is within the function, e.g.
All those checks are only done for dynamic access scopes, because they matter for runtime exclusivity checking.
Dead static scopes are removed unconditionally.
rdar://164571252