This is an archive of the discontinued LLVM Phabricator instance.

[ScalarEvolution] Predicate implication from operations
ClosedPublic

Authored by mkazantsev on Mar 13 2017, 5:49 AM.

Details

Summary

This patch allows SCEV predicate analysis to prove implication of some expression predicates
from context predicates related to arguments of those expressions.

It introduces three new rules:

For addition:

  1. (A >X && B >= 0) || (B >= 0 && A > X) ===> (A + B) > X.

For division:

  1. (A > X) && (0 < B <= X + 1) ===> (A / B > 0).
  2. (A > X) && (-B <= X < 0) ===> (A / B >= 0).

Using these rules, SCEV is able to prove facts like "if X > 1 then X / 2 > 0".
They can also be combined with the same context, to prove more complex expressions like
"if X > 1 then X/2 + 1 > 1".

Diff Detail

Event Timeline

mkazantsev created this revision.Mar 13 2017, 5:49 AM
sanjoy requested changes to this revision.Mar 14 2017, 6:39 PM

Hi Maxim,

First of all, great work!

Can you try to split this change up into the addition transform (which, as I said inline, probably makes sense to put in ScalarEvolution::isKnownPredicateViaNoOverflow) and the division transform? If one depends one the other, then we can just put denote the dependencies on phabricator, and check them in in the right order.

Other than that I have a few comments inline and here:

This rule seems a bit restrictive:

(A > X) && (B > X) && (A >= 0 || B >= 0) ===> (A + B) > X.

Why not:

(A >= 0 && B > X) ==> ((A +nsw B) > X?

(and the symmetric variant with A and B swapped)?

I've not checked if the code matches the commit message, but in

(A > X) && (-B <= X < 0) ===> (A / B > 0), what if X is -10, A is -5 and B is 10? Won't A / B be 0 then?

lib/Analysis/ScalarEvolution.cpp
8552

The usual way to do this is

if (Pred == ICmpInst::ICMP_SLT) {
  Pred = ICmpInst::ICMP_SGT;
  std::swap(LHS, RHS);
  std::swap(FoundLHS, FoundRHS);
}

Actually, you should not need to do this. ScalarEvolution::isImpliedCond should be handling this case for you.

8564

There already is getNoopOrSignExtend that does the same thing.

8570

Use getTypeSizeInBits(S1->getType()) instead, that will do the right thing for pointer types.

8582

Don't call it Operation, that name says nothing about where the value came from and what it represents. For a small scope I would not mind Operation, but this scope is more than a few lines long. I'd go with something more mnemonic, like AddExprLHS or LHSAddExpr.

8583

Did you consider putting this logic in ScalarEvolution::isKnownPredicateViaNoOverflow?

8586

Similarly, good names for these would be LL ("LHS of LHS"), and RL ("RHS of LHS").

8591

Use getZero(RHS->getType()).

8602

This is probably too expensive to put in here (at the very least, this is risky from a compile time perspective). How far can we get just by looking at the ranges for each of the inputs (i.e. via calling getRange and doing some manipulations on the returned ranges)?

8610

Same comment w.r.t. naming -- let's call it something more specific than Operation.

8616

Let's avoid creating SCEV expressions here (via getSCEV or getAddExpr). One problem is that it may be a compile time hit. They can also cause use to compute overly conservative trip counts, since this call to isImpliedViaOperations may have itself been done during a trip count computation, and invoking getSCEV may try to (recursively) compute the trip count of the same loop again which would cache a conservative SCEVCouldNotCompute (to avoid recursing infinitely).

This revision now requires changes to proceed.Mar 14 2017, 6:39 PM

Hi Sanjoy!

Thank you for review and detailed comment, I will try to address them all soon as I can!

Why not:
(A >= 0 && B > X) ==> ((A +nsw B) > X?

Good catch!

(A > X) && (-B <= X < 0) ===> (A / B > 0), what if X is -10, A is -5 and B is 10? Won't A / B be 0 then?

Thanks for pointing out, actuall the commit message is wrong. It should be "A / B >= 0". Line 8590 checks that RHS should be negative.

mkazantsev added inline comments.Mar 14 2017, 8:31 PM
lib/Analysis/ScalarEvolution.cpp
8552

I will check this. If you are right, checking "less" cases in switch of ScalarEvolution::isImpliedCondOperandsHelper seems redundant.

mkazantsev updated this revision to Diff 91835.Mar 15 2017, 1:53 AM
mkazantsev edited edge metadata.
mkazantsev marked 10 inline comments as done.
mkazantsev edited the summary of this revision. (Show Details)
mkazantsev added inline comments.Mar 15 2017, 1:57 AM
lib/Analysis/ScalarEvolution.cpp
8552

This is not true, we sometimes have "less" conditions at this point.

8583

Actually isKnownPredicateViaNoOverflow already does a particular case of it, which is "X + C > X if C > 0". The point here is that I need the context of FoundLHS and FounrRHS for further proofs. For example, I want to prove thing like "1 + n / 2 > 1 if n > 1". The rule which is used in isKnownPredicateViaNoOverflow does not work here, because n/2 does not match with 0 or 1. It also is unable to prove that n/2 > 0, because for this we need the proof via operation that uses context.

For the same reason I don't want to split this into two patches, because the only real benefit of the rule for addition is that it can recursively invoke the rule for division with the same context, and vice versa. Without the division rule, this addition work simply duplicates the logic of "isKnownPredicateViaNoOverflow".

I have removed "isKnownPredicate" invocation from here to avoid potential recursion, replacing it with more light-weight check. But we cannot move it out of here due to this context restriction.

8602

Ok, I got rid of recursive checks.

8616

I have removed the call of "isKnownPredicate" that might request recursive recalculation of the same trip count; now we use more light-weight check that used to be "IsKnownPredicateFull" lambda. So now this problem should have gone.

As for the compile time issue, what is the alternative to using SCEV for Num/Denum/FoundRHS+ 1?

mkazantsev edited the summary of this revision. (Show Details)Mar 15 2017, 1:59 AM
sanjoy requested changes to this revision.Mar 15 2017, 10:33 PM

Hi Max,

I have some more comments. This time I was not able to be as thorough as I wanted to be (got busy with other things during the day, and now it is bed time :) ), but hopefully whatever comments I have will let you make some progress.

lib/Analysis/ScalarEvolution.cpp
8583

I may have missed it, but I did not see (in the current version of the patch) where the division case calls into the addition case.

The addition case calling into the division case is fine, but I want to avoid "arbitrary recursion" by recursively calling into isImpliedCondOperandsHelper. Can you structure the code in a way that that doesn't happen? Maybe extract out the division case into a separate function that you directly call from here?

8616

One possibility would be to put the actual core of the logic in llvm::isImpliedCondition (which is in ValueTracking), and then try to call into that helper from there.

That is, say the antecedent is (sext i16 %t to i32) s< i32 44 and the consequent is %s != 400. We could then ask ValueTracking llvm::isImpliedCondition(i16 %t s< i16 44, %s != 400) [0] and return whatever ValueTracking told us.

We will probably need to generalize the interface of ValueTracking's llvm::isImpliedCondition a bit though, but that should be fine.

[0] Using the fact that (sext(A) s< sext(B)) == A s< B.

8689

Can we get what we want here without sign extension? As I've said below, sign extension can be expensive.

In fact, it would be surprising if we see LHS is not the same as OrigLHS since that would mean a sext (%a + %b)<nsw> did not get transformed to (sext %a + sext %b)<nsw> as per the rule in ScalarEvolution::getSignExtendExpr. That situation is possible, but should be rare.

8692

Add a one liner above this stating what this function is checking for. If you can give it a better name then that would be even better.

8711

This seems general enough to me that we should put this on ScalarEvolution itself, as Type *ScalarEvolution::getWiderType(Type *, Type *). It also makes isImpliedViaOperations less cluttered.

8720

Sign extending has the same problem as calling getSCEV (as you can probably tell from looking at ScalarEvolution::getSignExtendExpr, it can do a lot of work in the worst case). It isn't terrible because SCEV will cache the result in most cases once it has computed it, but we should try very hard to not call it so deep in the stack.

This revision now requires changes to proceed.Mar 15 2017, 10:33 PM
mkazantsev added inline comments.Mar 15 2017, 11:50 PM
lib/Analysis/ScalarEvolution.cpp
8689

Why is it rare? We can calculate sdiv i32 %a, %b and than use it in multiple ways, one of them being comparison to an i64 constant. In this case we will see exactly this.

sanjoy added inline comments.Mar 16 2017, 9:49 AM
lib/Analysis/ScalarEvolution.cpp
8689

Maybe we're misinterpreting each other, but I was specifically talking about this SCEVAddExpr case. That is, I'd be surprised if all of the following are simultaneously true:

  • LHS is a SCEVAddExpr marked as NSW
  • FoundLHS was a SCEVSignExtendExpr with LHS as its operand

because if they were, I'd have expected the sign extend to have been have been commuted to inside the add expression.

mkazantsev added inline comments.Mar 16 2017, 7:46 PM
lib/Analysis/ScalarEvolution.cpp
8689

Sorry, I misinterpreted it. Yes, this can be removed, I think.

mkazantsev marked 6 inline comments as done.Mar 16 2017, 10:47 PM
mkazantsev added inline comments.
lib/Analysis/ScalarEvolution.cpp
8616

I noticed that Num never needs a new SCEV creation in good case, because all we want is to prove that it's SCEV is actually FoundLHS. thus, it and some type conversions have gone.

Now we only have SCEV creation left for Denum-related stuff (such as constructing -Denum and Denum + 1). Here I believe that we cannot get rid of it, because for using implied conditions interfaces we will have to construct those sum and neg in terms of values if not in terms of SCEVs.

To avoid reculsive recalculations in this last case, let's just reduce the scope of the optimization to Denum being a constant. In this case creating Denum+1, -Denum or type extensions will only require constants creation, and there is a very high chance that these contant SCEVs already exist.

mkazantsev edited edge metadata.
mkazantsev marked 4 inline comments as done.

Addressed Sanjoy's comments. Made some generalization. Division rule now works for constant denumerator only to avoid recursive invocation of the analysis for the same loop.

Minor type mismatch bug fix.

sanjoy requested changes to this revision.Mar 19 2017, 9:57 PM

One more round of comments.

lib/Analysis/ScalarEvolution.cpp
8616

Your point is solid.

What do you think about creating a helper called (say) isSCEVSameAsValue(const SCEV *, const Value *) that checks cheaply (i.e. without creating new SCEV expressions) if a SCEV * and Value * compute the same thing at runtime? It would have to be best effort, but that's fine for now.

You can use this getExistingSCEV trick in that helper, and also use ExprValueMap to do the inverse.

8685

This should be called GetOpFromSExt.

8685

Why not just

return IsProvedViaContext(ICmpInst::ICMP_SGE, S1, getZero(RHS->getType()))) && IsProvedViaContext(Pred, S2, RHS);

?

Please also avoid using Pred in the second call to IsProvedViaContext, but use a literal ICmpInst::ICMP_SGT instead.

8700

Can we avoid the recursion via isImpliedCondOperandsHelper?

8717

This is minor, and I'll understand if you don't want to change it, but let's call Num Numerator. Num is too ambiguous -- it can also mean Number for instance.

Paradoxically, I think N and D is less ambiguous than Num and Denum. :)

I'd also call Denum Denom if you must use an abbreviation, since the full spelling is Denominator.

8732

Any reason why you need to check Denum <= FoundRHS + 1 instead of Denum < FoundRHS? Since FoundRHS < FoundLHS, FoundRHS + 1 can't sign overflow, so the above two should be equivalent with Denum < FoundRHS being (slightly) faster since we're not adding.

Can you also add one or two lines of comment as an informal proof on why this is correct?

Same for the second rule.

This revision now requires changes to proceed.Mar 19 2017, 9:57 PM
mkazantsev added inline comments.Mar 19 2017, 10:35 PM
lib/Analysis/ScalarEvolution.cpp
8732

Imagine Denum = 3, FoundRHS = 2. Denum <= FoundRHS + 1 is true, but Denum < FoundRHS is false. These two are not equivalent.

For example given that FoundRHS = 2. The given fact FoundLHS > 2 means that FoundLHS is at least 3. Then we can prove that FoundLHS / (2 + 1) is at least one. If we used you rule, we could only prove that FoundLHS / 1 > 0, which is a weaker statement.

I will add a comment on that proof.

sanjoy added inline comments.Mar 19 2017, 10:47 PM
lib/Analysis/ScalarEvolution.cpp
8711

It might be better to do auto *Denum = cast<SCEVConstant>(getSCEV(LR)).

8732

Yes, you're right -- they're not equivalent. I think I confused it with Denum + 1 <= FoundRHS.

On the other hand, can we write the condition as (Denum - 1) <= FoundRHS? Again, we know that Denum - 1 won't sign overflow, and computing (Denum - 1) may be faster than computing FoundRHS + 1 because Denum is a constant.

mkazantsev edited edge metadata.
mkazantsev marked 3 inline comments as done.

Addressed Sanjoy's new comments.

lib/Analysis/ScalarEvolution.cpp
8700

That's the point of the optimization! Sometimes we cannot simply prove that (a + b > c), but can do it via the context passed from division. And vice versa, if we have something like ( a / ( a / b + c)), we can prove the inner division using the context from outer division.

We are now not creating non-constant SCEVs, so all recursion will stay between the isImpliedCondOperandsHelper and isImpliedViaOperations, and we will always go down the syntax tree and never go into the infinite recursion. Actually its depth is not really big (not bigger than the depth of expressions).

8717

Shame on me! :D Thanks for pointing out.

mkazantsev marked an inline comment as done.Mar 19 2017, 11:59 PM
mkazantsev added inline comments.
lib/Analysis/ScalarEvolution.cpp
8732

Indeed, this makes sense. Will do.

mkazantsev marked 2 inline comments as done.
sanjoy accepted this revision.Mar 20 2017, 2:56 PM

This looks good to me.

I have two concerns that I've mentioned inline. Feel free to fix them however you see fit.

lib/Analysis/ScalarEvolution.cpp
8700

Do you rely only recursing into isImpliedViaOperations or into the whole of isImpliedCondOperandsHelper? If the former, I'd be much more comfortable if you:

  • Changed IsProvedViaContext to directly call into isImpliedViaOperations
  • Passed along a Depth parameter and cap it at a fairly low threshold (let's say 3?) to protect us from the truly pathological cases

We should implement the second point even if we need to recurse into isImpliedCondOperandsHelper, but if recursing into isImpliedViaOperations directly gives us what we want for cheap, we should just do that.

I'm not just worried about infinite recursion -- isImpliedCondOperandsHelper is called often enough that even a somewhat deep recursion here will slow things down.

test/Analysis/ScalarEvolution/scev-division.ll
9

Are you intentionally matching for Predicated backedge-taken count? I'd have expected you to match for just Loop %xxx: backedge-taken count is yyy etc.

This revision is now accepted and ready to land.Mar 20 2017, 2:56 PM
mkazantsev added inline comments.Mar 20 2017, 9:00 PM
lib/Analysis/ScalarEvolution.cpp
8700

The logic here is following: the chain isImpliedViaOperations -> IsProvedViaContext -> isImpliedViaOperations goes down the expression tree to its operands. This process cannot be infinite since it always goes only UP by CFG and never comes down through Phis. Its depth doesn't exceed the depth of expression tree.

This recursive chain does not prove anything by itself. The terminal facts it uses are proved in isImpliedCondOperandsHelper (via range analysis etc). So we cannot throw it away, since it is the essential part for proving the lowest-level facts.

I can add a depth here to avoid analyzing too big expression trees, though.

mkazantsev added inline comments.Mar 20 2017, 11:14 PM
lib/Analysis/ScalarEvolution.cpp
8700

UPD: I took a carefull look into it and now think that you are right. Seems that it is sufficient to have proofs without implication for terminal impressions which is done in isKnownViaSimpleReasoning. Context-biased analysis in helper seems redundant.

mkazantsev marked 3 inline comments as done.

Added threshold, slightly changed tests.

Did a quick rescan, LGTM again!

(You have commit access now, right?)

lib/Analysis/ScalarEvolution.cpp
8550

I'd write this as "We want to avoid hurting compile time ..."

8608

Indent is off?

mkazantsev updated this revision to Diff 92593.Mar 21 2017, 9:29 PM
mkazantsev marked 2 inline comments as done.

Thanks for review, Sanjoy! Yes, I have the rights. Let me try to merge it myself. :)

mkazantsev closed this revision.Mar 21 2017, 10:05 PM

Closed by the commit https://reviews.llvm.org/rL298481 [ScalarEvolution] Predicate implication from operations

mkazantsev added inline comments.Mar 22 2017, 4:24 AM
lib/Analysis/ScalarEvolution.cpp
8689

In fact, it would be surprising if we see LHS is not the same as OrigLHS since that would mean a sext (%a + %b)<nsw> did not get transformed to (sext %a + sext %b)<nsw> as per the rule in ScalarEvolution::getSignExtendExpr. That situation is possible, but should be rare.

It is possible indeed, and it lead to a crash on CLang built. I should prohibit it.