-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Cranelift: idempotent instructions have weird interactions with GVN #5796
Comments
cfallin
added a commit
to cfallin/wasmtime
that referenced
this issue
Feb 16, 2023
This PR addresses bytecodealliance#5796: currently, ops that are effectful, i.e., remain in the side-effecting skeleton (which we keep in the `Layout` while the egraph exists), but are idempotent and thus mergeable by a GVN pass, are not handled properly. GVN is still possible on effectful but idempotent ops precisely because our GVN does not create partial redundancies: it removes an instruction only when it is dominated by an identical instruction. An isntruction will not be "hoisted" to a point where it could execute in the optimized code but not in the original. However, there are really two parts to the egraph implementation that produce this effect: the deduplication on insertion into the egraph, and the elaboration with a scoped hashmap. The deduplication lets us give a single name (value ID) to all copies of an identical instruction, and then elaboration will re-create duplicates if GVN should not hoist or merge some of them. Because deduplication need not worry about dominance or scopes, we use a simple (non-scoped) hashmap to dedup/intern ops as "egraph nodes". When we added support for GVN'ing effectful but idempotent ops (bytecodealliance#5594), we kept the use of this simple dedup'ing hashmap, but these ops do not get elaborated; instead they stay in the side-effecting skeleton. Thus, we inadvertently created potential for weird code-motion effects. The proposal in bytecodealliance#5796 would solve this in a clean way by treating these ops as pure again, and keeping them out of the skeleton, instead putting "force" pseudo-ops in the skeleton. However, this is a little more complex than I would like, and I've realized that @jameysharp's earlier suggestion is much simpler: we can keep an actual scoped hashmap separately just for the effectful-but-idempotent ops, and use it to GVN while we build the egraph. In effect, we're fusing a separate GVN pass with the egraph pass (but letting it interact corecursively with egraph rewrites. This is in principle similar to how we keep a separate map for loads and fuse this pass with the egraph rewrite pass as well. Note that we can use a `ScopedHashMap` here without the "context" (as needed by `CtxHashMap`) because, as noted by @jameysharp, in practice the ops we want to GVN have all their args inline. Equality on the `InstructinoData` itself is conservative: two insts whose struct contents compare shallowly equal are definitely identical, but identical insts in a deep-equality sense may not compare shallowly equal, due to list indirection. This is fine for GVN, because it is still sound to skip any given GVN opportunity (and keep the original instructions). Fixes bytecodealliance#5796.
cfallin
added a commit
to cfallin/wasmtime
that referenced
this issue
Feb 16, 2023
This is a short-term fix to the same bug that bytecodealliance#5800 is addressing (bytecodealliance#5796), but with less risk: it simply turns off GVN'ing of effectful but idempotent ops. Because we have an upcoming release, and this is a miscompile (albeit to do with trapping behavior), we would like to make the simplest possible fix that avoids the bug, and backport it. I will then rebase bytecodealliance#5800 on top of a revert of this followed by the more complete fix.
cfallin
added a commit
to cfallin/wasmtime
that referenced
this issue
Feb 16, 2023
This is a short-term fix to the same bug that bytecodealliance#5800 is addressing (bytecodealliance#5796), but with less risk: it simply turns off GVN'ing of effectful but idempotent ops. Because we have an upcoming release, and this is a miscompile (albeit to do with trapping behavior), we would like to make the simplest possible fix that avoids the bug, and backport it. I will then rebase bytecodealliance#5800 on top of a revert of this followed by the more complete fix.
cfallin
added a commit
that referenced
this issue
Feb 16, 2023
This is a short-term fix to the same bug that #5800 is addressing (#5796), but with less risk: it simply turns off GVN'ing of effectful but idempotent ops. Because we have an upcoming release, and this is a miscompile (albeit to do with trapping behavior), we would like to make the simplest possible fix that avoids the bug, and backport it. I will then rebase #5800 on top of a revert of this followed by the more complete fix.
cfallin
added a commit
that referenced
this issue
Feb 16, 2023
This is a short-term fix to the same bug that #5800 is addressing (#5796), but with less risk: it simply turns off GVN'ing of effectful but idempotent ops. Because we have an upcoming release, and this is a miscompile (albeit to do with trapping behavior), we would like to make the simplest possible fix that avoids the bug, and backport it. I will then rebase #5800 on top of a revert of this followed by the more complete fix.
cfallin
added a commit
to cfallin/wasmtime
that referenced
this issue
Feb 16, 2023
This PR addresses bytecodealliance#5796: currently, ops that are effectful, i.e., remain in the side-effecting skeleton (which we keep in the `Layout` while the egraph exists), but are idempotent and thus mergeable by a GVN pass, are not handled properly. GVN is still possible on effectful but idempotent ops precisely because our GVN does not create partial redundancies: it removes an instruction only when it is dominated by an identical instruction. An isntruction will not be "hoisted" to a point where it could execute in the optimized code but not in the original. However, there are really two parts to the egraph implementation that produce this effect: the deduplication on insertion into the egraph, and the elaboration with a scoped hashmap. The deduplication lets us give a single name (value ID) to all copies of an identical instruction, and then elaboration will re-create duplicates if GVN should not hoist or merge some of them. Because deduplication need not worry about dominance or scopes, we use a simple (non-scoped) hashmap to dedup/intern ops as "egraph nodes". When we added support for GVN'ing effectful but idempotent ops (bytecodealliance#5594), we kept the use of this simple dedup'ing hashmap, but these ops do not get elaborated; instead they stay in the side-effecting skeleton. Thus, we inadvertently created potential for weird code-motion effects. The proposal in bytecodealliance#5796 would solve this in a clean way by treating these ops as pure again, and keeping them out of the skeleton, instead putting "force" pseudo-ops in the skeleton. However, this is a little more complex than I would like, and I've realized that @jameysharp's earlier suggestion is much simpler: we can keep an actual scoped hashmap separately just for the effectful-but-idempotent ops, and use it to GVN while we build the egraph. In effect, we're fusing a separate GVN pass with the egraph pass (but letting it interact corecursively with egraph rewrites. This is in principle similar to how we keep a separate map for loads and fuse this pass with the egraph rewrite pass as well. Note that we can use a `ScopedHashMap` here without the "context" (as needed by `CtxHashMap`) because, as noted by @jameysharp, in practice the ops we want to GVN have all their args inline. Equality on the `InstructinoData` itself is conservative: two insts whose struct contents compare shallowly equal are definitely identical, but identical insts in a deep-equality sense may not compare shallowly equal, due to list indirection. This is fine for GVN, because it is still sound to skip any given GVN opportunity (and keep the original instructions). Fixes bytecodealliance#5796.
cfallin
added a commit
that referenced
this issue
Mar 2, 2023
* Revert "egraphs: disable GVN of effectful idempotent ops (temporarily). (#5808)" This reverts commit c7e2571. * egraphs: fix handling of effectful-but-idempotent ops and GVN. This PR addresses #5796: currently, ops that are effectful, i.e., remain in the side-effecting skeleton (which we keep in the `Layout` while the egraph exists), but are idempotent and thus mergeable by a GVN pass, are not handled properly. GVN is still possible on effectful but idempotent ops precisely because our GVN does not create partial redundancies: it removes an instruction only when it is dominated by an identical instruction. An isntruction will not be "hoisted" to a point where it could execute in the optimized code but not in the original. However, there are really two parts to the egraph implementation that produce this effect: the deduplication on insertion into the egraph, and the elaboration with a scoped hashmap. The deduplication lets us give a single name (value ID) to all copies of an identical instruction, and then elaboration will re-create duplicates if GVN should not hoist or merge some of them. Because deduplication need not worry about dominance or scopes, we use a simple (non-scoped) hashmap to dedup/intern ops as "egraph nodes". When we added support for GVN'ing effectful but idempotent ops (#5594), we kept the use of this simple dedup'ing hashmap, but these ops do not get elaborated; instead they stay in the side-effecting skeleton. Thus, we inadvertently created potential for weird code-motion effects. The proposal in #5796 would solve this in a clean way by treating these ops as pure again, and keeping them out of the skeleton, instead putting "force" pseudo-ops in the skeleton. However, this is a little more complex than I would like, and I've realized that @jameysharp's earlier suggestion is much simpler: we can keep an actual scoped hashmap separately just for the effectful-but-idempotent ops, and use it to GVN while we build the egraph. In effect, we're fusing a separate GVN pass with the egraph pass (but letting it interact corecursively with egraph rewrites. This is in principle similar to how we keep a separate map for loads and fuse this pass with the egraph rewrite pass as well. Note that we can use a `ScopedHashMap` here without the "context" (as needed by `CtxHashMap`) because, as noted by @jameysharp, in practice the ops we want to GVN have all their args inline. Equality on the `InstructinoData` itself is conservative: two insts whose struct contents compare shallowly equal are definitely identical, but identical insts in a deep-equality sense may not compare shallowly equal, due to list indirection. This is fine for GVN, because it is still sound to skip any given GVN opportunity (and keep the original instructions). Fixes #5796. * Add comments from review.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@jameysharp brought to my attention an interesting case:
When run through the egraph framework, the
udiv
is sunk fromblock2
to two copies inblock3
andblock4
. This is code-motion of a side-effect that is actually, strictly speaking, incorrect. It won't cause a program that doesn't trap to trap, or a program that traps to not trap, but it may affect which trap is seen first and/or which side-effects occur prior to the trap.The basic issue is that we sort of treat idempotent operators as both pure -- so GVN'able -- but also as having a location, so remaining in the side-effect skeleton. This combines with the use of an ordinary map, not a scoped hashmap, for GVN'ing. That choice was valid when we processed only pure operators, which "float" outside of control flow and so have no need to limit their visibility to dominated blocks. But in the above program, the GVN map gets an entry when we see
v3
inblock1
, then we visitblock2
, see the GVN-map entry, rewritev4
tov3
, and remove the secondudiv
. Elaboration will then place copies of the udiv back inblock3
andblock4
as the value is used, because it already exists in the layout elsewhere. So we never use a value outside of its defined range (i.e., defs still dominate uses), but we do lose information about where the def occurs.@jameysharp and I were at first thinking we could add a scoped hashmap to not dedup
v3
andv4
to the same value, but actually this is suboptimal: the whole point of interning nodes in the egraph is to share the work of optimizing them once. And an idempotent op is mostly like a fully pure op, so we should be able to mostly reason about it in this shared way, except for where its side effects must have been made visible.A key insight for a better solution is: one can see any idempotent op as an actual pure op, with two outputs, the original and a "has trapped" flag; combined with an actual side-effecting op on the "has trapped" flag. We don't want to actually rewrite in this form. But in principle, the side-effect is separable in that way, and that gives us the GVN semantics that we desire.
The proposal is this: let idempotent ops GVN as pure ops do. Unconditionally remove them from the side-effect skeleton (the
Layout
). But in their place in theLayout
, insert aforce
pseudo-inst that uses the GVN'd value. The only purpose of this pseudo-op is to ensure that the value is computed at the given location during elaboration.This would give the desired behavior for the program above: it would retain the
udiv
in bothblock1
andblock2
, and would neither hoist nor lower it, while still GVN'ing both to one value that could be reasoned about / rewritten once.The text was updated successfully, but these errors were encountered: