ConstraintAnalysis: Copy constraints when copying locals#8888
Conversation
| // local x, and can copy its constraints. | ||
| if (c.op == Abstract::Eq) { | ||
| for (auto& otherC : get(*other)) { | ||
| approximateAnd(index, otherC); |
There was a problem hiding this comment.
It seems we could take everything we know about the current variable, AND it with everything we know about the other variable all at once rather than one constraint at at time (to take advantage of future heuristics about which constraints to keep), then update our knowledge for both the current and other variables to the combined constraint set.
For example, if we know that x <= 10 and y >= 10, then we learn that x == y, then we also know both x == 10 and y == 10.
There was a problem hiding this comment.
Oh, but in this case we are also overwriting the current variable, not just learning that it is equal to some other variable, so there is no current knowledge to apply to the other variable.
This would still be an interesting improvement, but this is not the place to handle it. Maybe we can handle both the case handled in this PR and that more general case in a single place where we learn about equality constraints?
There was a problem hiding this comment.
Hmm, I'm not sure where a better place would be? As you say, here we are overwriting, so doing a copy here in set() seems right?
There was a problem hiding this comment.
BasicBlockConstraintMap::approximateAnd(Index, const Constraint&) (or more accurately, approximateAndInternal) could do this whenever it sees an equality constraint. That will work for this case because BasicBlockConstraintMap::set already calls approximateAndInternal and it will also work for the case of learning an equality constraint without overwriting a variable.
There was a problem hiding this comment.
I see, good point. Doing this in approximateAnd would help the non-set case.
I did that in the last commit before the format, but it does turn out a little tricky: we need to handle recursion and some other things. Still, this now handles another case, so I think it's worth it.
| (i32.eq | ||
| (local.get $y) | ||
| (i32.const 10) |
There was a problem hiding this comment.
We could also test (i32.eq (local.get $x) (local.get $y)) (here and below).
There was a problem hiding this comment.
We already have testing of this above (it worked earlier). I can duplicate it here though if you think it helps readability?
tlively
left a comment
There was a problem hiding this comment.
LGTM.
Here's another design that could avoid wasting our limited constraint space with duplicated constraints due to equalities:
- Instead of mapping from local indices to constraint sets, we could could map from local indices to equivalence classes and then map equivalence classes to constraint sets.
- Looking up the constraints for a local would have just one extra step of looking up an equivalence class in the middle.
- Equality constraints between locals would never be stored as contraints; when they are encountered, the two locals' equivalence classes would be merged and the constraint set for the merged equivalence class would be the
approximateAndof the constraints for the two merged classes. - When a variable is overwritten, it is remapped to a fresh equivalence class.
This scheme would scale much better to programs where several variables are known to be equivalent to each other. We have support/disjoint_sets.h ready to go to represent equivalence classes.
| auto& indexConstraints = map[index]; | ||
| if (indexConstraints.provesEverything()) { | ||
| indexConstraints.setProvesNothing(); |
There was a problem hiding this comment.
This could be done more directly with map.insert({index, provesNothing}).
|
Equivalent classes can help, good idea. We already have a utility for that, EquivalentSets (used in SimplifyLocals etc.). I'll look into it later, not sure how common it is to have equivalent locals in optimized code. |
|
I took a look at |
I believe you suggested this @tlively , and it is necessary to handle real-world code.