blob: 8a80cd3d2368d0372986b656e59991a8d8a7dd91 [file] [log] [blame] [edit]
/*
* Copyright 2015 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Removes branches for which we go to where they go anyhow
//
#include <wasm.h>
#include <pass.h>
#include <parsing.h>
#include <ir/utils.h>
#include <ir/branch-utils.h>
#include <ir/effects.h>
#include <wasm-builder.h>
namespace wasm {
// to turn an if into a br-if, we must be able to reorder the
// condition and possible value, and the possible value must
// not have side effects (as they would run unconditionally)
static bool canTurnIfIntoBrIf(Expression* ifCondition, Expression* brValue, PassOptions& options) {
// if the if isn't even reached, this is all dead code anyhow
if (ifCondition->type == unreachable) return false;
if (!brValue) return true;
EffectAnalyzer value(options, brValue);
if (value.hasSideEffects()) return false;
return !EffectAnalyzer(options, ifCondition).invalidates(value);
}
struct RemoveUnusedBrs : public WalkerPass<PostWalker<RemoveUnusedBrs>> {
bool isFunctionParallel() override { return true; }
Pass* create() override { return new RemoveUnusedBrs; }
bool anotherCycle;
// Whether a value can flow in the current path. If so, then a br with value
// can be turned into a value, which will flow through blocks/ifs to the right place
bool valueCanFlow;
typedef std::vector<Expression**> Flows;
// list of breaks that are currently flowing. if they reach their target without
// interference, they can be removed (or their value forwarded TODO)
Flows flows;
// a stack for if-else contents, we merge their outputs
std::vector<Flows> ifStack;
// list of all loops, so we can optimize them
std::vector<Loop*> loops;
static void visitAny(RemoveUnusedBrs* self, Expression** currp) {
auto* curr = *currp;
auto& flows = self->flows;
if (curr->is<Break>()) {
flows.clear();
auto* br = curr->cast<Break>();
if (!br->condition) { // TODO: optimize?
// a break, let's see where it flows to
flows.push_back(currp);
self->valueCanFlow = true; // start optimistic
} else {
self->stopValueFlow();
}
} else if (curr->is<Return>()) {
flows.clear();
flows.push_back(currp);
self->valueCanFlow = true; // start optimistic
} else if (curr->is<If>()) {
auto* iff = curr->cast<If>();
if (iff->condition->type == unreachable) {
// avoid trying to optimize this, we never reach it anyhow
self->stopFlow();
return;
}
if (iff->ifFalse) {
assert(self->ifStack.size() > 0);
for (auto* flow : self->ifStack.back()) {
flows.push_back(flow);
}
self->ifStack.pop_back();
} else {
// if without else stops the flow of values
self->stopValueFlow();
}
} else if (curr->is<Block>()) {
// any breaks flowing to here are unnecessary, as we get here anyhow
auto* block = curr->cast<Block>();
auto name = block->name;
if (name.is()) {
size_t size = flows.size();
size_t skip = 0;
for (size_t i = 0; i < size; i++) {
auto* flow = (*flows[i])->dynCast<Break>();
if (flow && flow->name == name) {
if (!flow->value || self->valueCanFlow) {
if (!flow->value) {
// br => nop
ExpressionManipulator::nop<Break>(flow);
} else {
// br with value => value
*flows[i] = flow->value;
}
skip++;
self->anotherCycle = true;
}
} else if (skip > 0) {
flows[i - skip] = flows[i];
}
}
if (skip > 0) {
flows.resize(size - skip);
}
// drop a nop at the end of a block, which prevents a value flowing
while (block->list.size() > 0 && block->list.back()->is<Nop>()) {
block->list.resize(block->list.size() - 1);
self->anotherCycle = true;
}
}
} else if (curr->is<Nop>()) {
// ignore (could be result of a previous cycle)
self->stopValueFlow();
} else if (curr->is<Loop>()) {
// do nothing - it's ok for values to flow out
} else if (auto* sw = curr->dynCast<Switch>()) {
self->stopFlow();
self->optimizeSwitch(sw);
} else {
// anything else stops the flow
self->stopFlow();
}
}
void stopFlow() {
flows.clear();
valueCanFlow = false;
}
void stopValueFlow() {
flows.erase(std::remove_if(flows.begin(), flows.end(), [&](Expression** currp) {
auto* curr = *currp;
if (auto* ret = curr->dynCast<Return>()) {
return ret->value;
}
return curr->cast<Break>()->value;
}), flows.end());
valueCanFlow = false;
}
static void clear(RemoveUnusedBrs* self, Expression** currp) {
self->flows.clear();
}
static void saveIfTrue(RemoveUnusedBrs* self, Expression** currp) {
self->ifStack.push_back(std::move(self->flows));
}
void visitLoop(Loop* curr) {
loops.push_back(curr);
}
void optimizeSwitch(Switch* curr) {
// if the final element is the default, we don't need it
while (!curr->targets.empty() && curr->targets.back() == curr->default_) {
curr->targets.pop_back();
}
// if the first element is the default, we can remove it by shifting everything (which
// does add a subtraction of a constant, but often that is worth it as the constant can
// be folded away and/or we remove multiple elements here)
Index removable = 0;
while (removable < curr->targets.size() && curr->targets[removable] == curr->default_) {
removable++;
}
if (removable > 0) {
for (Index i = removable; i < curr->targets.size(); i++) {
curr->targets[i - removable] = curr->targets[i];
}
curr->targets.resize(curr->targets.size() - removable);
Builder builder(*getModule());
curr->condition = builder.makeBinary(SubInt32,
curr->condition,
builder.makeConst(Literal(int32_t(removable)))
);
}
// when there isn't a value, we can do some trivial optimizations without worrying about
// the value being executed before the condition
if (curr->value) return;
if (curr->targets.size() == 0) {
// a switch with just a default always goes there
Builder builder(*getModule());
replaceCurrent(builder.makeSequence(
builder.makeDrop(curr->condition),
builder.makeBreak(curr->default_)
));
} else if (curr->targets.size() == 1) {
// a switch with two options is basically an if
Builder builder(*getModule());
replaceCurrent(builder.makeIf(
curr->condition,
builder.makeBreak(curr->default_),
builder.makeBreak(curr->targets.front())
));
} else {
// there are also some other cases where we want to convert a switch into ifs,
// especially if the switch is large and we are focusing on size.
// an especially egregious case is a switch like this:
// [a b b [..] b b c] with default b
// (which may be arrived at after we trim away excess default values on both
// sides). in this case, we really have 3 values in a simple form, so it is the
// next logical case after handling 1 and 2 values right above here.
// to optimize this, we must add a local + a bunch of nodes (if*2, tee, eq,
// get, const, break*3), so the table must be big enough for it to make sense
// How many targets we need when shrinking. This is literally the size at which
// the transformation begins to be smaller.
const uint32_t MIN_SHRINK = 13;
// How many targets we need when not shrinking, in which case, 2 ifs may be slower,
// so we do this when the table is ridiculously large for one with just 3 values
// in it.
const uint32_t MIN_GENERAL = 128;
auto shrink = getPassRunner()->options.shrinkLevel > 0;
if ((curr->targets.size() >= MIN_SHRINK && shrink) ||
(curr->targets.size() >= MIN_GENERAL && !shrink)) {
for (Index i = 1; i < curr->targets.size() - 1; i++) {
if (curr->targets[i] != curr->default_) {
return;
}
}
// great, we are in that case, optimize
Builder builder(*getModule());
auto temp = builder.addVar(getFunction(), i32);
Expression* z;
replaceCurrent(z = builder.makeIf(
builder.makeTeeLocal(temp, curr->condition),
builder.makeIf(
builder.makeBinary(EqInt32,
builder.makeGetLocal(temp, i32),
builder.makeConst(Literal(int32_t(curr->targets.size() - 1)))
),
builder.makeBreak(curr->targets.back()),
builder.makeBreak(curr->default_)
),
builder.makeBreak(curr->targets.front())
));
}
}
}
void visitIf(If* curr) {
if (!curr->ifFalse) {
// if without an else. try to reduce if (condition) br => br_if (condition)
Break* br = curr->ifTrue->dynCast<Break>();
if (br && !br->condition) { // TODO: if there is a condition, join them
if (canTurnIfIntoBrIf(curr->condition, br->value, getPassOptions())) {
br->condition = curr->condition;
br->finalize();
replaceCurrent(Builder(*getModule()).dropIfConcretelyTyped(br));
anotherCycle = true;
}
}
}
// TODO: if-else can be turned into a br_if as well, if one of the sides is a dead end
// we handle the case of a returned value to a set_local later down, see
// visitSetLocal.
}
// override scan to add a pre and a post check task to all nodes
static void scan(RemoveUnusedBrs* self, Expression** currp) {
self->pushTask(visitAny, currp);
auto* iff = (*currp)->dynCast<If>();
if (iff) {
if (iff->condition->type == unreachable) {
// avoid trying to optimize this, we never reach it anyhow
return;
}
self->pushTask(doVisitIf, currp);
if (iff->ifFalse) {
// we need to join up if-else control flow, and clear after the condition
self->pushTask(scan, &iff->ifFalse);
self->pushTask(saveIfTrue, currp); // safe the ifTrue flow, we'll join it later
}
self->pushTask(scan, &iff->ifTrue);
self->pushTask(clear, currp); // clear all flow after the condition
self->pushTask(scan, &iff->condition);
} else {
super::scan(self, currp);
}
}
// optimizes a loop. returns true if we made changes
bool optimizeLoop(Loop* loop) {
// if a loop ends in
// (loop $in
// (block $out
// if (..) br $in; else br $out;
// )
// )
// then our normal opts can remove the break out since it flows directly out
// (and later passes make the if one-armed). however, the simple analysis
// fails on patterns like
// if (..) br $out;
// br $in;
// which is a common way to do a while (1) loop (end it with a jump to the
// top), so we handle that here. Specifically we want to conditionalize
// breaks to the loop top, i.e., put them behind a condition, so that other
// code can flow directly out and thus brs out can be removed. (even if
// the change is to let a break somewhere else flow out, that can still be
// helpful, as it shortens the logical loop. it is also good to generate
// an if-else instead of an if, as it might allow an eqz to be removed
// by flipping arms)
if (!loop->name.is()) return false;
auto* block = loop->body->dynCast<Block>();
if (!block) return false;
// does the last element break to the top of the loop?
auto& list = block->list;
if (list.size() <= 1) return false;
auto* last = list.back()->dynCast<Break>();
if (!last || !ExpressionAnalyzer::isSimple(last) || last->name != loop->name) return false;
// last is a simple break to the top of the loop. if we can conditionalize it,
// it won't block things from flowing out and not needing breaks to do so.
Index i = list.size() - 2;
Builder builder(*getModule());
while (1) {
auto* curr = list[i];
if (auto* iff = curr->dynCast<If>()) {
// let's try to move the code going to the top of the loop into the if-else
if (!iff->ifFalse) {
// we need the ifTrue to break, so it cannot reach the code we want to move
if (iff->ifTrue->type == unreachable) {
iff->ifFalse = builder.stealSlice(block, i + 1, list.size());
iff->finalize();
block->finalize();
return true;
}
} else {
// this is already an if-else. if one side is a dead end, we can append to the other, if
// there is no returned value to concern us
assert(!isConcreteType(iff->type)); // can't be, since in the middle of a block
// ensures the first node is a block, if it isn't already, and merges in the second,
// either as a single element or, if a block, by appending to the first block. this
// keeps the order of operations in place, that is, the appended element will be
// executed after the first node's elements
auto blockifyMerge = [&](Expression* any, Expression* append) -> Block* {
Block* block = nullptr;
if (any) block = any->dynCast<Block>();
// if the first isn't a block, or it's a block with a name (so we might
// branch to the end, and so can't append to it, we might skip that code!)
// then make a new block
if (!block || block->name.is()) {
block = builder.makeBlock(any);
} else {
assert(!isConcreteType(block->type));
}
auto* other = append->dynCast<Block>();
if (!other) {
block->list.push_back(append);
} else {
for (auto* item : other->list) {
block->list.push_back(item);
}
}
block->finalize();
return block;
};
if (iff->ifTrue->type == unreachable) {
iff->ifFalse = blockifyMerge(iff->ifFalse, builder.stealSlice(block, i + 1, list.size()));
iff->finalize();
block->finalize();
return true;
} else if (iff->ifFalse->type == unreachable) {
iff->ifTrue = blockifyMerge(iff->ifTrue, builder.stealSlice(block, i + 1, list.size()));
iff->finalize();
block->finalize();
return true;
}
}
return false;
} else if (auto* brIf = curr->dynCast<Break>()) {
// br_if is similar to if.
if (brIf->condition && !brIf->value && brIf->name != loop->name) {
if (i == list.size() - 2) {
// there is the br_if, and then the br to the top, so just flip them and the condition
brIf->condition = builder.makeUnary(EqZInt32, brIf->condition);
last->name = brIf->name;
brIf->name = loop->name;
return true;
} else {
// there are elements in the middle,
// br_if $somewhere (condition)
// (..more..)
// br $in
// we can convert the br_if to an if. this has a cost, though,
// so only do it if it looks useful, which it definitely is if
// (a) $somewhere is straight out (so the br out vanishes), and
// (b) this br_if is the only branch to that block (so the block will vanish)
if (brIf->name == block->name && BranchUtils::BranchSeeker::countNamed(block, block->name) == 1) {
// note that we could drop the last element here, it is a br we know for sure is removable,
// but telling stealSlice to steal all to the end is more efficient, it can just truncate.
list[i] = builder.makeIf(brIf->condition, builder.makeBreak(brIf->name), builder.stealSlice(block, i + 1, list.size()));
return true;
}
}
}
return false;
}
// if there is control flow, we must stop looking
if (EffectAnalyzer(getPassOptions(), curr).branches) {
return false;
}
if (i == 0) return false;
i--;
}
}
void doWalkFunction(Function* func) {
// multiple cycles may be needed
bool worked = false;
do {
anotherCycle = false;
super::doWalkFunction(func);
assert(ifStack.empty());
// flows may contain returns, which are flowing out and so can be optimized
for (size_t i = 0; i < flows.size(); i++) {
auto* flow = (*flows[i])->dynCast<Return>();
if (!flow) continue;
if (!flow->value) {
// return => nop
ExpressionManipulator::nop(flow);
anotherCycle = true;
} else if (valueCanFlow) {
// return with value => value
*flows[i] = flow->value;
anotherCycle = true;
}
}
flows.clear();
// optimize loops (we don't do it while tracking flows, as they can interfere)
for (auto* loop : loops) {
anotherCycle |= optimizeLoop(loop);
}
loops.clear();
if (anotherCycle) worked = true;
} while (anotherCycle);
if (worked) {
// Our work may alter block and if types, they may now return values that we made flow through them
ReFinalize().walkFunctionInModule(func, getModule());
}
// thread trivial jumps
struct JumpThreader : public ControlFlowWalker<JumpThreader> {
// map of all value-less breaks going to a block (and not a loop)
std::map<Block*, std::vector<Break*>> breaksToBlock;
// the names to update
std::map<Break*, Name> newNames;
void visitBreak(Break* curr) {
if (!curr->value) {
if (auto* target = findBreakTarget(curr->name)->dynCast<Block>()) {
breaksToBlock[target].push_back(curr);
}
}
}
// TODO: Switch?
void visitBlock(Block* curr) {
auto& list = curr->list;
if (list.size() == 1 && curr->name.is()) {
// if this block has just one child, a sub-block, then jumps to the former are jumps to us, really
if (auto* child = list[0]->dynCast<Block>()) {
// the two blocks must have the same type for us to update the branch, as otherwise
// one block may be unreachable and the other concrete, so one might lack a value
if (child->name.is() && child->name != curr->name && child->type == curr->type) {
auto& breaks = breaksToBlock[child];
for (auto* br : breaks) {
newNames[br] = curr->name;
breaksToBlock[curr].push_back(br); // update the list - we may push it even more later
}
breaksToBlock.erase(child);
}
}
} else if (list.size() == 2) {
// if this block has two children, a child-block and a simple jump, then jumps to child-block can be replaced with jumps to the new target
auto* child = list[0]->dynCast<Block>();
auto* jump = list[1]->dynCast<Break>();
if (child && child->name.is() && jump && ExpressionAnalyzer::isSimple(jump)) {
auto& breaks = breaksToBlock[child];
for (auto* br : breaks) {
newNames[br] = jump->name;
}
// if the jump is to another block then we can update the list, and maybe push it even more later
if (auto* newTarget = findBreakTarget(jump->name)->dynCast<Block>()) {
for (auto* br : breaks) {
breaksToBlock[newTarget].push_back(br);
}
}
breaksToBlock.erase(child);
}
}
}
void finish(Function* func) {
for (auto& iter : newNames) {
auto* br = iter.first;
auto name = iter.second;
br->name = name;
}
if (newNames.size() > 0) {
// by changing where brs go, we may change block types etc.
ReFinalize().walkFunctionInModule(func, getModule());
}
}
};
JumpThreader jumpThreader;
jumpThreader.setModule(getModule());
jumpThreader.walkFunction(func);
jumpThreader.finish(func);
// perform some final optimizations
struct FinalOptimizer : public PostWalker<FinalOptimizer> {
bool shrink;
PassOptions& passOptions;
bool needUniqify = false;
FinalOptimizer(PassOptions& passOptions) : passOptions(passOptions) {}
void visitBlock(Block* curr) {
// if a block has an if br else br, we can un-conditionalize the latter, allowing
// the if to become a br_if.
// * note that if not in a block already, then we need to create a block for this, so not useful otherwise
// * note that this only happens at the end of a block, as code after the if is dead
// * note that we do this at the end, because un-conditionalizing can interfere with optimizeLoop()ing.
auto& list = curr->list;
for (Index i = 0; i < list.size(); i++) {
auto* iff = list[i]->dynCast<If>();
if (!iff || !iff->ifFalse || isConcreteType(iff->type)) continue; // if it lacked an if-false, it would already be a br_if, as that's the easy case
auto* ifTrueBreak = iff->ifTrue->dynCast<Break>();
if (ifTrueBreak && !ifTrueBreak->condition && canTurnIfIntoBrIf(iff->condition, ifTrueBreak->value, passOptions)) {
// we are an if-else where the ifTrue is a break without a condition, so we can do this
ifTrueBreak->condition = iff->condition;
ifTrueBreak->finalize();
list[i] = Builder(*getModule()).dropIfConcretelyTyped(ifTrueBreak);
ExpressionManipulator::spliceIntoBlock(curr, i + 1, iff->ifFalse);
continue;
}
// otherwise, perhaps we can flip the if
auto* ifFalseBreak = iff->ifFalse->dynCast<Break>();
if (ifFalseBreak && !ifFalseBreak->condition && canTurnIfIntoBrIf(iff->condition, ifFalseBreak->value, passOptions)) {
ifFalseBreak->condition = Builder(*getModule()).makeUnary(EqZInt32, iff->condition);
ifFalseBreak->finalize();
list[i] = Builder(*getModule()).dropIfConcretelyTyped(ifFalseBreak);
ExpressionManipulator::spliceIntoBlock(curr, i + 1, iff->ifTrue);
continue;
}
}
if (list.size() >= 2) {
// combine/optimize adjacent br_ifs + a br (maybe _if) right after it
for (Index i = 0; i < list.size() - 1; i++) {
auto* br1 = list[i]->dynCast<Break>();
// avoid unreachable brs, as they are dead code anyhow, and after merging
// them the outer scope could need type changes
if (!br1 || !br1->condition || br1->type == unreachable) continue;
assert(!br1->value);
auto* br2 = list[i + 1]->dynCast<Break>();
if (!br2 || br1->name != br2->name) continue;
assert(!br2->value); // same target as previous, which has no value
// a br_if and then a br[_if] with the same target right after it
if (br2->condition) {
if (shrink && br2->type != unreachable) {
// Join adjacent br_ifs to the same target, making one br_if with
// a "selectified" condition that executes both.
if (!EffectAnalyzer(passOptions, br2->condition).hasSideEffects()) {
// it's ok to execute them both, do it
Builder builder(*getModule());
br1->condition = builder.makeBinary(OrInt32, br1->condition, br2->condition);
ExpressionManipulator::nop(br2);
}
}
} else {
// merge, we go there anyhow
Builder builder(*getModule());
list[i] = builder.makeDrop(br1->condition);
}
}
// combine adjacent br_ifs that test the same value into a br_table,
// when that makes sense
tablify(curr);
// Restructuring of ifs: if we have
// (block $x
// (br_if $x (cond))
// .., no other references to $x
// )
// then we can turn that into (if (!cond) ..).
// Code size wise, we turn the block into an if (no change), and
// lose the br_if (-2). .. turns into the body of the if in the binary
// format. We need to flip the condition, which at worst adds 1.
if (curr->name.is()) {
auto* br = list[0]->dynCast<Break>();
// we seek a regular br_if; if the type is unreachable that means it is not
// actually reached, so ignore
if (br && br->condition && br->name == curr->name && br->type != unreachable) {
assert(!br->value); // can't, it would be dropped or last in the block
if (BranchUtils::BranchSeeker::countNamed(curr, curr->name) == 1) {
// no other breaks to that name, so we can do this
Builder builder(*getModule());
replaceCurrent(builder.makeIf(
builder.makeUnary(EqZInt32, br->condition),
curr
));
curr->name = Name();
ExpressionManipulator::nop(br);
curr->finalize(curr->type);
return;
}
}
}
}
}
void visitIf(If* curr) {
// we may have simplified ifs enough to turn them into selects
// this is helpful for code size, but can be a tradeoff with performance as we run both code paths
if (!shrink) return;
if (curr->ifFalse && isConcreteType(curr->ifTrue->type) && isConcreteType(curr->ifFalse->type)) {
// if with else, consider turning it into a select if there is no control flow
// TODO: estimate cost
EffectAnalyzer condition(passOptions, curr->condition);
if (!condition.hasSideEffects()) {
EffectAnalyzer ifTrue(passOptions, curr->ifTrue);
if (!ifTrue.hasSideEffects()) {
EffectAnalyzer ifFalse(passOptions, curr->ifFalse);
if (!ifFalse.hasSideEffects()) {
auto* select = getModule()->allocator.alloc<Select>();
select->condition = curr->condition;
select->ifTrue = curr->ifTrue;
select->ifFalse = curr->ifFalse;
select->finalize();
replaceCurrent(select);
}
}
}
}
}
void visitSetLocal(SetLocal* curr) {
// Undo an if return value into a local, if one arm is a br, as we
// prefer a br_if:
// (set_local $x
// (if (result i32)
// (..condition..)
// (br $somewhere)
// (..result)
// )
// )
// =>
// (br_if $somewhere
// (..condition..)
// )
// (set_local $x
// (..result)
// )
// TODO: handle a condition in the br? need to watch for side effects
auto* iff = curr->value->dynCast<If>();
if (!iff) return;
if (!isConcreteType(iff->type) || !isConcreteType(iff->condition->type)) return;
auto tryToOptimize = [&](Expression* one, Expression* two, bool flipCondition) {
if (one->type == unreachable && two->type != unreachable) {
if (auto* br = one->dynCast<Break>()) {
if (ExpressionAnalyzer::isSimple(br)) {
// Wonderful, do it!
Builder builder(*getModule());
br->condition = iff->condition;
if (flipCondition) {
br->condition = builder.makeUnary(EqZInt32, br->condition);
}
br->finalize();
curr->value = two;
replaceCurrent(
builder.makeSequence(
br,
curr
)
);
return true;
}
}
}
return false;
};
if (!tryToOptimize(iff->ifTrue, iff->ifFalse, false)) {
tryToOptimize(iff->ifFalse, iff->ifTrue, true);
}
}
// (br_if)+ => br_table
// we look for the specific pattern of
// (br_if ..target1..
// (i32.eq
// (..input..)
// (i32.const ..value1..)
// )
// )
// (br_if ..target2..
// (i32.eq
// (..input..)
// (i32.const ..value2..)
// )
// )
// TODO: consider also looking at <= etc. and not just eq
void tablify(Block* block) {
auto &list = block->list;
if (list.size() <= 1) return;
// Heuristics. These are slightly inspired by the constants from the asm.js backend.
// How many br_ifs we need to see to consider doing this
const uint32_t MIN_NUM = 3;
// How much of a range of values is definitely too big
const uint32_t MAX_RANGE = 1024;
// Multiplied by the number of br_ifs, then compared to the range. When
// this is high, we allow larger ranges.
const uint32_t NUM_TO_RANGE_FACTOR = 3;
// check if the input is a proper br_if on an i32.eq of a condition value to a const,
// and the const is in the proper range, [0-int32_max), to avoid overflow concerns.
// returns the br_if if so, or nullptr otherwise
auto getProperBrIf = [](Expression* curr) -> Break*{
auto* br = curr->dynCast<Break>();
if (!br) return nullptr;
if (!br->condition || br->value) return nullptr;
if (br->type != none) return nullptr; // no value, so can be unreachable or none. ignore unreachable ones, dce will clean it up
auto* binary = br->condition->dynCast<Binary>();
if (!binary) return nullptr;
if (binary->op != EqInt32) return nullptr;
auto* c = binary->right->dynCast<Const>();
if (!c) return nullptr;
uint32_t value = c->value.geti32();
if (value >= uint32_t(std::numeric_limits<int32_t>::max())) return nullptr;
return br;
};
// check if the input is a proper br_if
// and returns the condition if so, or nullptr otherwise
auto getProperBrIfConditionValue = [&getProperBrIf](Expression* curr) -> Expression* {
auto* br = getProperBrIf(curr);
if (!br) return nullptr;
return br->condition->cast<Binary>()->left;
};
// returns the constant value, as a uint32_t
auto getProperBrIfConstant = [&getProperBrIf](Expression* curr) -> uint32_t {
return getProperBrIf(curr)->condition->cast<Binary>()->right->cast<Const>()->value.geti32();
};
Index start = 0;
while (start < list.size() - 1) {
auto* conditionValue = getProperBrIfConditionValue(list[start]);
if (!conditionValue) {
start++;
continue;
}
// if the condition has side effects, we can't replace many appearances of it
// with a single one
if (EffectAnalyzer(passOptions, conditionValue).hasSideEffects()) {
start++;
continue;
}
// look for a "run" of br_ifs with all the same conditionValue, and having
// unique constants (an overlapping constant could be handled, just the first
// branch is taken, but we can't remove the other br_if (it may be the only
// branch keeping a block reachable), which may make this bad for code size.
Index end = start + 1;
std::unordered_set<uint32_t> usedConstants;
usedConstants.insert(getProperBrIfConstant(list[start]));
while (end < list.size() &&
ExpressionAnalyzer::equal(getProperBrIfConditionValue(list[end]),
conditionValue)) {
if (!usedConstants.insert(getProperBrIfConstant(list[end])).second) {
// this constant already appeared
break;
}
end++;
}
auto num = end - start;
if (num >= 2 && num >= MIN_NUM) {
// we found a suitable range, [start, end), containing more than 1
// element. let's see if it's worth it
auto min = getProperBrIfConstant(list[start]);
auto max = min;
for (Index i = start + 1; i < end; i++) {
auto* curr = list[i];
min = std::min(min, getProperBrIfConstant(curr));
max = std::max(max, getProperBrIfConstant(curr));
}
uint32_t range = max - min;
// decision time
if (range <= MAX_RANGE &&
range <= num * NUM_TO_RANGE_FACTOR) {
// great! let's do this
std::unordered_set<Name> usedNames;
for (Index i = start; i < end; i++) {
usedNames.insert(getProperBrIf(list[i])->name);
}
// we need a name for the default too
Name defaultName;
Index i = 0;
while (1) {
defaultName = "tablify|" + std::to_string(i++);
if (usedNames.count(defaultName) == 0) break;
}
std::vector<Name> table;
for (Index i = start; i < end; i++) {
auto name = getProperBrIf(list[i])->name;
auto index = getProperBrIfConstant(list[i]);
index -= min;
while (table.size() <= index) {
table.push_back(defaultName);
}
assert(table[index] == defaultName); // we should have made sure there are no overlaps
table[index] = name;
}
Builder builder(*getModule());
// the table and condition are offset by the min
if (min != 0) {
conditionValue = builder.makeBinary(
SubInt32,
conditionValue,
builder.makeConst(Literal(int32_t(min)))
);
}
list[end - 1] = builder.makeBlock(
defaultName,
builder.makeSwitch(
table,
defaultName,
conditionValue
)
);
for (Index i = start; i < end - 1; i++) {
ExpressionManipulator::nop(list[i]);
}
// the defaultName may exist elsewhere in this function,
// uniquify it later
needUniqify = true;
}
}
start = end;
}
}
};
FinalOptimizer finalOptimizer(getPassOptions());
finalOptimizer.setModule(getModule());
finalOptimizer.shrink = getPassRunner()->options.shrinkLevel > 0;
finalOptimizer.walkFunction(func);
if (finalOptimizer.needUniqify) {
wasm::UniqueNameMapper::uniquify(func->body);
}
}
};
Pass *createRemoveUnusedBrsPass() {
return new RemoveUnusedBrs();
}
} // namespace wasm