egraphs: fix handling of effectful-but-idempotent ops and GVN. (#5800)
* Revert "egraphs: disable GVN of effectful idempotent ops (temporarily). (#5808)"
This reverts commit c7e2571866.
* 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.
This commit is contained in:
@@ -73,6 +73,25 @@ pub fn is_pure_for_egraph(func: &Function, inst: Inst) -> bool {
|
||||
has_one_result && (is_readonly_load || (!op.can_load() && !trivially_has_side_effects(op)))
|
||||
}
|
||||
|
||||
/// Can the given instruction be merged into another copy of itself?
|
||||
/// These instructions may have side-effects, but as long as we retain
|
||||
/// the first instance of the instruction, the second and further
|
||||
/// instances are redundant if they would produce the same trap or
|
||||
/// result.
|
||||
pub fn is_mergeable_for_egraph(func: &Function, inst: Inst) -> bool {
|
||||
let op = func.dfg.insts[inst].opcode();
|
||||
// We can only merge one-result operators due to the way that GVN
|
||||
// is structured in the egraph implementation.
|
||||
let has_one_result = func.dfg.inst_results(inst).len() == 1;
|
||||
has_one_result
|
||||
// Loads/stores are handled by alias analysis and not
|
||||
// otherwise mergeable.
|
||||
&& !op.can_load()
|
||||
&& !op.can_store()
|
||||
// Can only have idempotent side-effects.
|
||||
&& (!has_side_effect(func, inst) || op.side_effects_idempotent())
|
||||
}
|
||||
|
||||
/// Does the given instruction have any side-effect as per [has_side_effect], or else is a load,
|
||||
/// but not the get_pinned_reg opcode?
|
||||
pub fn has_lowering_side_effect(func: &Function, inst: Inst) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user