diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index b2b018500d3..e837e21ca71 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -517,6 +517,107 @@ void BasicBlockConstraintMap::set(Index index, const Constraint& c) { approximateAnd(index, c); } +void BasicBlockConstraintMap::set(Index index, + const AndedConstraintSet& constraints) { + // As above, but with a loop after. + assert(!unreachable); + eraseStaleRefs(index); + map.erase(index); + + // Apply the constraints, if there are any. + if (constraints.provesNothing()) { + setProvesNothing(index); + } else { + for (auto& c : constraints) { + approximateAnd(index, c); + } + } +} + +void BasicBlockConstraintMap::set(Index index, Expression* value) { + using namespace Match; + using namespace Abstract; + + // Apply a constraint to a value, x = C. + if (Properties::isSingleConstantExpression(value)) { + auto c = Properties::getLiteral(value); + set(index, Constraint{Abstract::Eq, {c}}); + return; + } + + // Apply a constraint to a local, x = y. + if (auto* get = value->dynCast()) { + set(index, Constraint{Abstract::Eq, {get->index}}); + return; + } + + // Apply an increment of a local, x = y + 1. + Index y; + if (matches(value, binary(Abstract::Add, local(&y), ival(1)))) { + // The local y must have old constraints that we know how to increment. + auto old = get(y); + + // Iterate over the old constraints and increment each one. + auto success = true; + for (auto& c : old) { + auto* N = std::get_if(&c.term); + if (!N) { + // A non-constant term, which we don't know how to increment. + success = false; + break; + } + + switch (c.op) { + // x == N, x++ => x == N+1. + case Eq: + *N = N->add(Literal::makeFromInt32(1, N->type)); + continue; + // x >= N, x++ => x > N + case GeS: + c.op = GtS; + continue; + case GeU: + c.op = GtU; + continue; + // x < N, x++ => x <= N + case LtS: + c.op = LeS; + continue; + case LtU: + c.op = LeU; + continue; + // x <= N, x++ => x <= N+1 if no overflow + case LeS: + if (N->isSignedMax()) { + success = false; + break; + } + *N = N->add(Literal::makeFromInt32(1, N->type)); + continue; + case LeU: + if (N->isUnsignedMax()) { + success = false; + break; + } + *N = N->add(Literal::makeFromInt32(1, N->type)); + continue; + default: + // Something we don't recognize. + success = false; + break; + } + } + + if (success) { + set(index, old); + return; + } + } + + // We know and can prove nothing. + setProvesNothing(index); +} + void BasicBlockConstraintMap::setProvesNothing(Index index) { assert(!unreachable); eraseStaleRefs(index); diff --git a/src/ir/constraint.h b/src/ir/constraint.h index 9fea231fbc7..07d4254cb7e 100644 --- a/src/ir/constraint.h +++ b/src/ir/constraint.h @@ -251,9 +251,15 @@ struct BasicBlockConstraintMap { assert(map.empty()); } - // Apply a constraint to a local. + // Apply a constraint to a local, replacing anything before. void set(Index index, const Constraint& c); + // Apply a set of constraints to a local, replacing anything before. + void set(Index index, const AndedConstraintSet& constraints); + + // Set the value in an expression to a local, replacing anything before. + void set(Index index, Expression* value); + // Mark a local as unknown and able to prove nothing. void setProvesNothing(Index index); diff --git a/src/ir/match.h b/src/ir/match.h index 383ff8d057a..a60bad43235 100644 --- a/src/ir/match.h +++ b/src/ir/match.h @@ -613,6 +613,18 @@ SelectMatcher(Select** binder, S1&& s1, S2&& s2, S3&& s3) { return Matcher(binder, {}, s1, s2, s3); } +// LocalGet +template<> struct NumComponents { + static constexpr size_t value = 1; +}; +template<> struct GetComponent { + Index operator()(LocalGet* curr) { return curr->index; } +}; +template +inline decltype(auto) LocalGetMatcher(LocalGet** binder, S&& s) { + return Matcher(binder, {}, s); +} + } // namespace Internal // Public matching API @@ -878,6 +890,13 @@ inline decltype(auto) select(Select** binder, S1&& s1, S2&& s2, S3&& s3) { return Internal::SelectMatcher(binder, s1, s2, s3); } +inline decltype(auto) local() { + return Internal::LocalGetMatcher(nullptr, Internal::Any(nullptr)); +} +inline decltype(auto) local(Index* binder) { + return Internal::LocalGetMatcher(nullptr, Internal::Any(binder)); +} + } // namespace wasm::Match #endif // wasm_ir_match_h diff --git a/src/passes/ConstraintAnalysis.cpp b/src/passes/ConstraintAnalysis.cpp index 5eb6a24fcf4..e2dddb9d67d 100644 --- a/src/passes/ConstraintAnalysis.cpp +++ b/src/passes/ConstraintAnalysis.cpp @@ -36,6 +36,12 @@ #include "wasm-builder.h" #include "wasm.h" +#define CONSTRAINT_DEBUG 0 + +#ifndef CONSTRAINT_DEBUG +#define CONSTRAINT_DEBUG 0 +#endif + namespace wasm { using namespace wasm::constraint; @@ -187,6 +193,7 @@ struct ConstraintAnalysis } computeRelevantLocals(); + prepareToFlow(); flow(); optimize(); } @@ -217,9 +224,42 @@ struct ConstraintAnalysis } } + // Maintain a maximum amount of operations. The one non-linear thing that can + // happen is when we increment a local in a loop: it may go from 0 to 1, then + // branch back to the top and merge, making it in the range [0, 1], then get + // incremented and loop again, leading to [0, 2] and so forth, only stopping + // when it reaches the loop bound, which may be very high. We don't want to + // spend significant time on such constant operations, as other passes will + // propagate them anyhow, so we keep our time bounded. When this reaches 0, + // we will not do loop operations that might lead to such incrementing. + Index maxWorkLeft = 0; + + void prepareToFlow() { + // Compute a bound for maxOperations. flow() will spend time on each block, + // operation in a block, and branch, so add all those up. + for (auto& block : basicBlocks) { + maxWorkLeft += 1 + block->contents.actions.size() + block->out.size(); + } + + // We also allow a multiple of all the above: loop optimization generally + // requires us to process it twice (so that we see the merge at the top). + // Use a constant of 3 to make sure to work enough. + maxWorkLeft *= 3; + } + + void decMaxWork() { + if (maxWorkLeft > 0) { + maxWorkLeft--; + } + } + // Flow infos around until we have inferred all we can about the constraints // in each location. void flow() { +#if CONSTRAINT_DEBUG + dumpCFG("flow"); +#endif + // Start from the entry as the only reachable block. That block has incoming // values - defaults - for each var. entry->contents.startConstraints.setReachable(); @@ -247,18 +287,34 @@ struct ConstraintAnalysis // Starting from the entry, keep going while we find something new. UniqueDeferredQueue work; work.push(entry); + while (!work.empty()) { auto* block = work.pop(); + decMaxWork(); + // Start at the top of the block, then go through, applying things. BasicBlockConstraintMap constraints = block->contents.startConstraints; + +#if CONSTRAINT_DEBUG + std::cout << block << " start constraints: " << constraints << '\n'; +#endif + for (auto** currp : block->contents.actions) { applyToConstraints(*currp, constraints); + + decMaxWork(); } +#if CONSTRAINT_DEBUG + std::cout << block << " end constraints: " << constraints << '\n'; +#endif + // We now know the values at the end of the block. Flow it onward, and // where it causes changes, queue more work. for (auto* out : block->out) { + decMaxWork(); + auto& outStartConstraints = out->contents.startConstraints; // Find the constraints sent to this specific successor, if there is a @@ -267,14 +323,27 @@ struct ConstraintAnalysis branch && checkRelevancy(*branch)) { auto sentConstraints = constraints; sentConstraints.approximateAnd(branch->local, branch->constraint); +#if CONSTRAINT_DEBUG + std::cout << block << " sending branch to " << out + << " with sent constraints: " << sentConstraints << '\n'; +#endif // If anything changed at the start of the target block, flow onwards. if (outStartConstraints.approximateOr(sentConstraints)) { +#if CONSTRAINT_DEBUG + std::cout << "out's start after " << outStartConstraints << '\n'; + std::cout << block << " branch-modified " << out + << " to start with: " << outStartConstraints << '\n'; +#endif work.push(out); } } else { // There are no specific branch constraints, so send the unmodified // |constraints|, avoiding a copy. if (outStartConstraints.approximateOr(constraints)) { +#if CONSTRAINT_DEBUG + std::cout << block << " modified " << out + << " to start with: " << outStartConstraints << '\n'; +#endif work.push(out); } } @@ -293,6 +362,9 @@ struct ConstraintAnalysis // of course not needed at this stage.) auto& constraints = block->contents.startConstraints; for (auto** currp : block->contents.actions) { +#if CONSTRAINT_DEBUG + std::cout << block << " trying to optimize " << **currp << '\n'; +#endif if (!constraints.unreachable) { applyToConstraints(*currp, constraints); optimizeExpression(currp, constraints); @@ -432,17 +504,16 @@ struct ConstraintAnalysis // No point to apply a constraint to an irrelevant local. return; } - if (Properties::isSingleConstantExpression(set->value)) { - // Apply a constraint to this value. - auto value = Properties::getLiteral(set->value); - constraints.set(set->index, Constraint{Abstract::Eq, {value}}); - } else if (auto* get = set->value->dynCast()) { - // Apply a constraint to this local. - constraints.set(set->index, Constraint{Abstract::Eq, {get->index}}); - } else { - // We know and can prove nothing. + + // The only binary operation we match is an increment (x + 1), and we do + // not always want to apply it: only when we are allowed to keep working + // (see above). + if (set->value->is() && !maxWorkLeft) { constraints.setProvesNothing(set->index); + return; } + + constraints.set(set->index, set->value); } } diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp index 0b534d12efb..cad0b0b678e 100644 --- a/test/gtest/constraint.cpp +++ b/test/gtest/constraint.cpp @@ -503,3 +503,132 @@ TEST(ConstraintTest, TestAndLoop) { // x <= y && { x < y && x != 42 } => x < y && x != 42 checkAnd(ley, {lty[0], ne42}, {lty[0], ne42}); } + +TEST(ConstraintTest, TestBasicBlockConstraintMap) { + // Maps begin unreachable. + BasicBlockConstraintMap map; + + EXPECT_TRUE(map.unreachable); + map.setReachable(); + EXPECT_FALSE(map.unreachable); +} + +// Check that a set is equal to a constraint. +static void check(const AndedConstraintSet& s, const Constraint& c) { + EXPECT_EQ(s.size(), 1); + EXPECT_EQ(s[0], c); +} + +TEST(ConstraintTest, TestBasicBlockConstraintMap_Set) { + Constraint eq0{Eq, {Literal(int32_t(0))}}; + Constraint eq1{Eq, {Literal(int32_t(1))}}; + Constraint eq2{Eq, {Literal(int32_t(2))}}; + + BasicBlockConstraintMap map; + map.setReachable(); + + // Set local 0 to 0. It should read back the same. + map.set(0, eq0); + check(map.get(0), eq0); + + // Set another value, replacing the first. + map.set(0, eq1); + check(map.get(0), eq1); + + // Set a value using an expression. + Const c; + c.value = Literal(int32_t(2)); + c.type = Type::i32; + map.set(0, &c); + check(map.get(0), eq2); + + // Set an unfamiliar expression, leading to us knowing nothing. + Nop nop; + map.set(0, &nop); + EXPECT_TRUE(map.get(0).provesNothing()); +} + +TEST(ConstraintTest, TestIncrement) { + BasicBlockConstraintMap map; + map.setReachable(); + + // Set up an increment operation, an add which does $0 + 1 + LocalGet get; + get.index = 0; + get.type = Type::i32; + + Const c; + c.value = Literal(int32_t(1)); + c.type = Type::i32; + + Binary add; + add.op = AddInt32; + add.type = Type::i32; + add.left = &get; + add.right = &c; + + // $0 = 0, $1 = $0 + 1, so $1 = 1 (and $0 is unchanged). + map.set(0, {Eq, {Literal(int32_t(0))}}); + map.set(1, &add); + check(map.get(0), {Eq, {Literal(int32_t(0))}}); + check(map.get(1), {Eq, {Literal(int32_t(1))}}); + + // $0 = $0 + 1, where $0 was 0, so it is now 1. + map.set(0, &add); + check(map.get(0), {Eq, {Literal(int32_t(1))}}); + + // $0 >= 5, $0++ => $0 > 5 (signed) + map.set(0, {GeS, {Literal(int32_t(5))}}); + map.set(0, &add); + check(map.get(0), {GtS, {Literal(int32_t(5))}}); + + // Ditto, unsigned + map.set(0, {GeU, {Literal(int32_t(5))}}); + map.set(0, &add); + check(map.get(0), {GtU, {Literal(int32_t(5))}}); + + // $0 < 5, $0++ => $0 <= 5 (signed) + map.set(0, {LtS, {Literal(int32_t(5))}}); + map.set(0, &add); + check(map.get(0), {LeS, {Literal(int32_t(5))}}); + + // Ditto, unsigned + map.set(0, {LtU, {Literal(int32_t(5))}}); + map.set(0, &add); + check(map.get(0), {LeU, {Literal(int32_t(5))}}); + + // $0 <= 5, $0++ => $0 <= 6 (signed) + map.set(0, {LeS, {Literal(int32_t(5))}}); + map.set(0, &add); + check(map.get(0), {LeS, {Literal(int32_t(6))}}); + + // Ditto, unsigned + map.set(0, {LeU, {Literal(int32_t(5))}}); + map.set(0, &add); + check(map.get(0), {LeU, {Literal(int32_t(6))}}); + + // $0 <= max_signed, $0++ => nothing, because it would overflow + map.set(0, {LeS, {Literal::makeSignedMax(Type::i32)}}); + map.set(0, &add); + EXPECT_TRUE(map.get(0).provesNothing()); + + // $0 <= max_unsigned, $0++ => nothing, because it would overflow + map.set(0, {LeU, {Literal::makeUnsignedMax(Type::i32)}}); + map.set(0, &add); + EXPECT_TRUE(map.get(0).provesNothing()); + + // However, an unsigned operation on the signed max is fine. + map.set(0, {LeU, {Literal::makeSignedMax(Type::i32)}}); + map.set(0, &add); + auto one = Literal::makeFromInt32(1, Type::i32); + check(map.get(0), {LeU, {Literal::makeSignedMax(Type::i32).add(one)}}); + + // Multiple constraints at once: + // $0 >= 10 && $0 < 20, $0++ => $0 > 10 && $0 <= 20 + map.set(0, {GeS, {Literal(int32_t(10))}}); + map.approximateAnd(0, {LtS, {Literal(int32_t(20))}}); + map.set(0, &add); + EXPECT_EQ(map.get(0), + (AndedConstraintSet{{GtS, {Literal(int32_t(10))}}, + {LeS, {Literal(int32_t(20))}}})); +} diff --git a/test/lit/passes/constraint-analysis-loops.wast b/test/lit/passes/constraint-analysis-loops.wast index 5d90de1096f..8545fa734e1 100644 --- a/test/lit/passes/constraint-analysis-loops.wast +++ b/test/lit/passes/constraint-analysis-loops.wast @@ -6,6 +6,64 @@ ;; CHECK: (import "a" "b" (func $import (type $1) (result i32))) (import "a" "b" (func $import (result i32))) + ;; CHECK: (func $infinite-loop (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $infinite-loop + (local $x i32) + ;; An infinite loop. We should not hang, but nothing can be optimized. + (loop $loop + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + (br $loop) + ) + ) + + ;; CHECK: (func $almost-infinite-loop (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $loop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $almost-infinite-loop + (local $x i32) + ;; A loop that continues until an overflow happens. We should not hang, but + ;; nothing can be optimized. + (loop $loop + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + ;; Stop looping after we go all the way back to 0. + (br_if $loop + (local.get $x) + ) + ) + ) + ;; CHECK: (func $bound (type $0) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (loop $loop