summaryrefslogtreecommitdiff
path: root/src/passes/GlobalStructInference.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/passes/GlobalStructInference.cpp')
-rw-r--r--src/passes/GlobalStructInference.cpp282
1 files changed, 220 insertions, 62 deletions
diff --git a/src/passes/GlobalStructInference.cpp b/src/passes/GlobalStructInference.cpp
index c0b92e3bb..0ddbe9b0f 100644
--- a/src/passes/GlobalStructInference.cpp
+++ b/src/passes/GlobalStructInference.cpp
@@ -48,8 +48,11 @@
// TODO: Only do the case with a select when shrinkLevel == 0?
//
+#include <variant>
+
#include "ir/find_all.h"
#include "ir/module-utils.h"
+#include "ir/names.h"
#include "ir/possible-constant.h"
#include "ir/subtypes.h"
#include "ir/utils.h"
@@ -211,16 +214,98 @@ struct GlobalStructInference : public Pass {
std::sort(globals.begin(), globals.end());
}
- // Optimize based on the above.
- struct FunctionOptimizer
- : public WalkerPass<PostWalker<FunctionOptimizer>> {
- bool isFunctionParallel() override { return true; }
+ // We are looking for the case where we can pick between two values using a
+ // single comparison. More than two values, or more than a single
+ // comparison, lead to tradeoffs that may not be worth it.
+ //
+ // Note that situation may involve more than two globals. For example we may
+ // have three relevant globals, but two may have the same value. In that
+ // case we can compare against the third:
+ //
+ // $global0: (struct.new $Type (i32.const 42))
+ // $global1: (struct.new $Type (i32.const 42))
+ // $global2: (struct.new $Type (i32.const 1337))
+ //
+ // (struct.get $Type (ref))
+ // =>
+ // (select
+ // (i32.const 1337)
+ // (i32.const 42)
+ // (ref.eq (ref) $global2))
+ //
+ // To discover these situations, we compute and group the possible values
+ // that can be read from a particular struct.get, using the following data
+ // structure.
+ struct Value {
+ // A value is either a constant, or if not, then we point to whatever
+ // expression it is.
+ std::variant<PossibleConstantValues, Expression*> content;
+ // The list of globals that have this Value. In the example from above,
+ // the Value for 42 would list globals = [$global0, $global1].
+ // TODO: SmallVector?
+ std::vector<Name> globals;
+
+ bool isConstant() const {
+ return std::get_if<PossibleConstantValues>(&content);
+ }
- std::unique_ptr<Pass> create() override {
- return std::make_unique<FunctionOptimizer>(parent);
+ const PossibleConstantValues& getConstant() const {
+ assert(isConstant());
+ return std::get<PossibleConstantValues>(content);
}
- FunctionOptimizer(GlobalStructInference& parent) : parent(parent) {}
+ Expression* getExpression() const {
+ assert(!isConstant());
+ return std::get<Expression*>(content);
+ }
+ };
+
+ // Constant expressions are easy to handle, and we can emit a select as in
+ // the last example. But we can handle non-constant ones too, by un-nesting
+ // the relevant global. Imagine we have this:
+ //
+ // (global $g (struct.new $S
+ // (struct.new $T ..)
+ //
+ // We have a nested struct.new here. That is not a constant value, but we
+ // can turn it into a global.get:
+ //
+ // (global $g.nested (struct.new $T ..)
+ // (global $g (struct.new $S
+ // (global.get $g.nested)
+ //
+ // After this un-nesting we end up with a global.get of an immutable global,
+ // which is constant. Note that this adds a global and may increase code
+ // size slightly, but if it lets us infer constant values that may lead to
+ // devirtualization and other large benefits. Later passes can also re-nest.
+ //
+ // We do most of our optimization work in parallel, but we cannot add
+ // globals in parallel, so instead we note the places we need to un-nest in
+ // this data structure and process them at the end.
+ struct GlobalToUnnest {
+ // The global we want to refer to a nested part of, by un-nesting it. The
+ // global contains a struct.new, and we want to refer to one of the
+ // operands of the struct.new directly, which we can do by moving it out
+ // to its own new global.
+ Name global;
+ // The index of the struct.new in the global named |global|.
+ Index index;
+ // The global.get that should refer to the new global. At the end, after
+ // we create a new global and have a name for it, we update this get to
+ // point to it.
+ GlobalGet* get;
+ };
+ using GlobalsToUnnest = std::vector<GlobalToUnnest>;
+
+ struct FunctionOptimizer : PostWalker<FunctionOptimizer> {
+ private:
+ GlobalStructInference& parent;
+ GlobalsToUnnest& globalsToUnnest;
+
+ public:
+ FunctionOptimizer(GlobalStructInference& parent,
+ GlobalsToUnnest& globalsToUnnest)
+ : parent(parent), globalsToUnnest(globalsToUnnest) {}
bool refinalize = false;
@@ -278,65 +363,85 @@ struct GlobalStructInference : public Pass {
return;
}
- // We are looking for the case where we can pick between two values
- // using a single comparison. More than two values, or more than a
- // single comparison, add tradeoffs that may not be worth it, and a
- // single value (or no value) is already handled by other passes.
- //
- // That situation may involve more than two globals. For example we may
- // have three relevant globals, but two may have the same value. In that
- // case we can compare against the third:
- //
- // $global0: (struct.new $Type (i32.const 42))
- // $global1: (struct.new $Type (i32.const 42))
- // $global2: (struct.new $Type (i32.const 1337))
- //
- // (struct.get $Type (ref))
- // =>
- // (select
- // (i32.const 1337)
- // (i32.const 42)
- // (ref.eq (ref) $global2))
-
- // Find the constant values and which globals correspond to them.
- // TODO: SmallVectors?
- std::vector<PossibleConstantValues> values;
- std::vector<std::vector<Name>> globalsForValue;
-
- // Check if the relevant fields contain constants.
+ // TODO: SmallVector?
+ std::vector<Value> values;
+
+ // Scan the relevant struct.new operands.
auto fieldType = field.type;
for (Index i = 0; i < globals.size(); i++) {
Name global = globals[i];
auto* structNew = wasm.getGlobal(global)->init->cast<StructNew>();
- PossibleConstantValues value;
+ // The value that is read from this struct.new.
+ Value value;
+
+ // Find the value read from the struct and represent it as a Value.
+ PossibleConstantValues constant;
if (structNew->isWithDefault()) {
- value.note(Literal::makeZero(fieldType));
+ constant.note(Literal::makeZero(fieldType));
+ value.content = constant;
} else {
- value.note(structNew->operands[fieldIndex], wasm);
- if (!value.isConstant()) {
- // Give up entirely.
- return;
+ Expression* operand = structNew->operands[fieldIndex];
+ constant.note(operand, wasm);
+ if (constant.isConstant()) {
+ value.content = constant;
+ } else {
+ value.content = operand;
}
}
- // Process the current value, comparing it against the previous.
- auto found = std::find(values.begin(), values.end(), value);
- if (found == values.end()) {
- // This is a new value.
- assert(values.size() <= 2);
+ // If the value is constant, it may be grouped as mentioned before.
+ // See if it matches anything we've seen before.
+ bool grouped = false;
+ if (value.isConstant()) {
+ for (auto& oldValue : values) {
+ if (oldValue.isConstant() &&
+ oldValue.getConstant() == value.getConstant()) {
+ // Add us to this group.
+ oldValue.globals.push_back(global);
+ grouped = true;
+ break;
+ }
+ }
+ }
+ if (!grouped) {
+ // This is a new value, so create a new group, unless we've seen too
+ // many unique values. In that case, give up.
if (values.size() == 2) {
- // Adding this value would mean we have too many, so give up.
return;
}
+ value.globals.push_back(global);
values.push_back(value);
- globalsForValue.push_back({global});
- } else {
- // This is an existing value.
- Index index = found - values.begin();
- globalsForValue[index].push_back(global);
}
}
+ // Helper for optimization: Given a Value, returns what we should read
+ // for it.
+ auto getReadValue = [&](const Value& value) -> Expression* {
+ if (value.isConstant()) {
+ // This is known to be a constant, so simply emit an expression for
+ // that constant.
+ return value.getConstant().makeExpression(wasm);
+ }
+
+ // Otherwise, this is non-constant, so we are in the situation where
+ // we want to un-nest the value out of the struct.new it is in. Note
+ // that for later work, as we cannot add a global in parallel.
+
+ // There can only be one global in a value that is not constant, which
+ // is the global we want to read from.
+ assert(value.globals.size() == 1);
+
+ // Create a global.get with temporary name, leaving only the updating
+ // of the name to later work.
+ auto* get = builder.makeGlobalGet(value.globals[0],
+ value.getExpression()->type);
+
+ globalsToUnnest.emplace_back(
+ GlobalToUnnest{value.globals[0], fieldIndex, get});
+
+ return get;
+ };
+
// We have some globals (at least 2), and so must have at least one
// value. And we have already exited if we have more than 2 values (see
// the early return above) so that only leaves 1 and 2.
@@ -345,7 +450,7 @@ struct GlobalStructInference : public Pass {
// otherwise return the value.
replaceCurrent(builder.makeSequence(
builder.makeDrop(builder.makeRefAs(RefAsNonNull, curr->ref)),
- values[0].makeExpression(wasm)));
+ getReadValue(values[0])));
return;
}
assert(values.size() == 2);
@@ -353,11 +458,11 @@ struct GlobalStructInference : public Pass {
// We have two values. Check that we can pick between them using a
// single comparison. While doing so, ensure that the index we can check
// on is 0, that is, the first value has a single global.
- if (globalsForValue[0].size() == 1) {
+ if (values[0].globals.size() == 1) {
// The checked global is already in index 0.
- } else if (globalsForValue[1].size() == 1) {
+ } else if (values[1].globals.size() == 1) {
+ // Flip so the value to check is in index 0.
std::swap(values[0], values[1]);
- std::swap(globalsForValue[0], globalsForValue[1]);
} else {
// Both indexes have more than one option, so we'd need more than one
// comparison. Give up.
@@ -365,15 +470,19 @@ struct GlobalStructInference : public Pass {
}
// Excellent, we can optimize here! Emit a select.
- //
+
+ auto checkGlobal = values[0].globals[0];
+ // Compute the left and right values before the next line, as the order
+ // of their execution matters (they may note globals for un-nesting).
+ auto* left = getReadValue(values[0]);
+ auto* right = getReadValue(values[1]);
// Note that we must trap on null, so add a ref.as_non_null here.
- auto checkGlobal = globalsForValue[0][0];
replaceCurrent(builder.makeSelect(
builder.makeRefEq(builder.makeRefAs(RefAsNonNull, curr->ref),
builder.makeGlobalGet(
checkGlobal, wasm.getGlobal(checkGlobal)->type)),
- values[0].makeExpression(wasm),
- values[1].makeExpression(wasm)));
+ left,
+ right));
}
void visitFunction(Function* func) {
@@ -381,12 +490,61 @@ struct GlobalStructInference : public Pass {
ReFinalize().walkFunctionInModule(func, getModule());
}
}
-
- private:
- GlobalStructInference& parent;
};
- FunctionOptimizer(*this).run(getPassRunner(), module);
+ // Find the optimization opportunitites in parallel.
+ ModuleUtils::ParallelFunctionAnalysis<GlobalsToUnnest> optimization(
+ *module, [&](Function* func, GlobalsToUnnest& globalsToUnnest) {
+ if (func->imported()) {
+ return;
+ }
+
+ FunctionOptimizer optimizer(*this, globalsToUnnest);
+ optimizer.walkFunctionInModule(func, module);
+ });
+
+ // Un-nest any globals as needed, using the deterministic order of the
+ // functions in the module.
+ Builder builder(*module);
+ auto addedGlobals = false;
+ for (auto& func : module->functions) {
+ // Each work item here is a global with a struct.new, from which we want
+ // to read a particular index, from a particular global.get.
+ for (auto& [globalName, index, get] : optimization.map[func.get()]) {
+ auto* global = module->getGlobal(globalName);
+ auto* structNew = global->init->cast<StructNew>();
+ assert(index < structNew->operands.size());
+ auto*& operand = structNew->operands[index];
+
+ // If we already un-nested this then we don't need to repeat that work.
+ if (auto* nestedGet = operand->dynCast<GlobalGet>()) {
+ // We already un-nested, and this global.get refers to the new global.
+ // Simply copy the target.
+ get->name = nestedGet->name;
+ assert(get->type == nestedGet->type);
+ } else {
+ // Add a new global, initialized to the operand.
+ auto newName = Names::getValidGlobalName(
+ *module,
+ global->name.toString() + ".unnested." + std::to_string(index));
+ module->addGlobal(builder.makeGlobal(
+ newName, get->type, operand, Builder::Immutable));
+ // Replace the operand with a get of that new global, and update the
+ // original get to read the same.
+ operand = builder.makeGlobalGet(newName, get->type);
+ get->name = newName;
+ addedGlobals = true;
+ }
+ }
+ }
+
+ if (addedGlobals) {
+ // Sort the globals so that added ones appear before their uses.
+ PassRunner runner(module);
+ runner.add("reorder-globals-always");
+ runner.setIsNested(true);
+ runner.run();
+ }
}
};