summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ir/manipulation.h4
-rw-r--r--src/ir/module-utils.cpp11
-rw-r--r--src/ir/module-utils.h7
-rw-r--r--src/passes/Monomorphize.cpp517
4 files changed, 456 insertions, 83 deletions
diff --git a/src/ir/manipulation.h b/src/ir/manipulation.h
index 33c7d1bd7..64cd15dc3 100644
--- a/src/ir/manipulation.h
+++ b/src/ir/manipulation.h
@@ -64,6 +64,10 @@ inline OutputType* convert(InputType* input, MixedArena& allocator) {
return output;
}
+// Copy using a flexible custom copy function. This function is called on each
+// expression before copying it. If it returns a non-null value then that is
+// used (effectively overriding the normal copy), and if it is null then we do a
+// normal copy.
using CustomCopier = std::function<Expression*(Expression*)>;
Expression*
flexibleCopy(Expression* original, Module& wasm, CustomCopier custom);
diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp
index 5791ed77c..8ad127086 100644
--- a/src/ir/module-utils.cpp
+++ b/src/ir/module-utils.cpp
@@ -46,6 +46,15 @@ Function* copyFunction(Function* func,
Module& out,
Name newName,
std::optional<std::vector<Index>> fileIndexMap) {
+ auto ret = copyFunctionWithoutAdd(func, out, newName, fileIndexMap);
+ return out.addFunction(std::move(ret));
+}
+
+std::unique_ptr<Function>
+copyFunctionWithoutAdd(Function* func,
+ Module& out,
+ Name newName,
+ std::optional<std::vector<Index>> fileIndexMap) {
auto ret = std::make_unique<Function>();
ret->name = newName.is() ? newName : func->name;
ret->hasExplicitName = func->hasExplicitName;
@@ -71,7 +80,7 @@ Function* copyFunction(Function* func,
ret->base = func->base;
ret->noFullInline = func->noFullInline;
ret->noPartialInline = func->noPartialInline;
- return out.addFunction(std::move(ret));
+ return ret;
}
Global* copyGlobal(Global* global, Module& out) {
diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h
index 0a101e5da..d9fd69428 100644
--- a/src/ir/module-utils.h
+++ b/src/ir/module-utils.h
@@ -33,6 +33,13 @@ copyFunction(Function* func,
Name newName = Name(),
std::optional<std::vector<Index>> fileIndexMap = std::nullopt);
+// As above, but does not add the copy to the module.
+std::unique_ptr<Function> copyFunctionWithoutAdd(
+ Function* func,
+ Module& out,
+ Name newName = Name(),
+ std::optional<std::vector<Index>> fileIndexMap = std::nullopt);
+
Global* copyGlobal(Global* global, Module& out);
Tag* copyTag(Tag* tag, Module& out);
diff --git a/src/passes/Monomorphize.cpp b/src/passes/Monomorphize.cpp
index 012f24750..5237f7864 100644
--- a/src/passes/Monomorphize.cpp
+++ b/src/passes/Monomorphize.cpp
@@ -15,24 +15,58 @@
*/
//
-// When we see a call foo(arg1, arg2) and at least one of the arguments has a
-// more refined type than is declared in the function being called, create a
-// copy of the function with the refined type. That copy can then potentially be
-// optimized in useful ways later.
+// Monomorphization of code based on callsite context: When we see a call, see
+// if the information at the callsite can help us optimize. For example, if a
+// parameter is constant, then using that constant in the called function may
+// unlock a lot of improvements. We may benefit from monomorphizing in the
+// following cases:
//
-// Inlining also monomorphizes in effect. What this pass does is handle the
-// cases where inlining cannot be done.
+// * If a call provides a more refined type than the function declares for a
+// parameter.
+// * If a call provides a constant as a parameter.
+// * If a call provides a GC allocation as a parameter. TODO
+// * If a call is dropped. TODO also other stuff on the outside?
//
-// To see when monomorphizing makes sense, this optimizes the target function
-// both with and without the more refined types. If the refined types help then
-// the version with might remove a cast, for example. Note that while doing so
-// we keep the optimization results of the version without - there is no reason
-// to forget them since we've gone to the trouble anyhow. So this pass may have
-// the side effect of performing minor optimizations on functions. There is also
-// a variant of the pass that always monomorphizes, even when it does not seem
-// helpful, which is useful for testing, and possibly in cases where we need
-// more than just local optimizations to see the benefit - for example, perhaps
-// GUFA ends up more powerful later on.
+// We realize the benefit by creating a monomorphized (specialized/refined)
+// version of the function, and call that instead. For example, if we have
+//
+// function foo(x) { return x + 22; }
+// foo(7);
+//
+// then monomorphization leads to this:
+//
+// function foo(x) { return x + 22; } // original unmodified function
+// foo_b(); // now calls foo_b
+// function foo_b() { return 7 + 22; } // monomorphized, constant 7 applied
+//
+// This is related to inlining both conceptually and practically. Conceptually,
+// one of inlining's big advantages is that we then optimize the called code
+// together with the code around the call, and monomorphization does something
+// similar. And, this pass does so by "reverse-inlining" content from the
+// caller to the monomorphized function: the constant 7 in the example above has
+// been "pulled in" from the caller into the callee. Larger amounts of code can
+// be moved in that manner, both values sent to the function, and the code that
+// receives it (see the mention of dropped calls, before).
+//
+// As this monormophization uses callsite context (the parameters, where the
+// result flows to), we call it "Contextual Monomorphization." The full name is
+// "Empirical Contextural Monomorphization" because we decide where to optimize
+// based on a "try it and see" (empirical) approach, that measures the benefit.
+// That is, we generate the monomorphized function as explained, then optimize
+// that function, which contains the original code + code from the callsite
+// context that we pulled in. If the optimizer manages to improve that combined
+// code in a useful way then we apply the optimization, and if not then we undo.
+//
+// The empirical approach significantly reduces the need for heuristics. For
+// example, rather than have a heuristic for "see if a constant parameter flows
+// into a conditional branch," we simply run the optimizer and let it optimize
+// that case. All other cases handled by the optimizer work as well, without
+// needing to specify them as heuristics, so this gets smarter as the optimizer
+// does.
+//
+// Aside from the main version of this pass there is also a variant useful for
+// testing that always monomorphizes non-trivial callsites, without checking if
+// the optimizer can help or not (that makes writing testcases simpler).
//
// TODO: When we optimize we could run multiple cycles: A calls B calls C might
// end up with the refined+optimized B now having refined types in its
@@ -47,25 +81,21 @@
// end on leaves. That would make it more likely for a single iteration to
// do more work, as if A->B->C then we'd do A->B and optimize B and only
// then look at B->C.
-// TODO: Also run the result-refining part of SignatureRefining, as if we
-// refine the result then callers of the function may benefit, even if
-// there is no benefit in the function itself.
// TODO: If this is too slow, we could "group" things, for example we could
// compute the LUB of a bunch of calls to a target and then investigate
// that one case and use it in all those callers.
// TODO: Not just direct calls? But updating vtables is complex.
-// TODO: Not just types? We could monomorphize using Literal values. E.g. for
-// function references, if we monomorphized we'd end up specializing qsort
-// for the particular functions it is given.
//
#include "ir/cost.h"
#include "ir/find_all.h"
+#include "ir/manipulation.h"
#include "ir/module-utils.h"
#include "ir/names.h"
#include "ir/type-updating.h"
#include "ir/utils.h"
#include "pass.h"
+#include "support/hash.h"
#include "wasm-type.h"
#include "wasm.h"
@@ -73,18 +103,200 @@ namespace wasm {
namespace {
+// Relevant information about a callsite for purposes of monomorphization.
+struct CallContext {
+ // The operands of the call, processed to leave the parts that make sense to
+ // keep in the context. That is, the operands of the CallContext are the exact
+ // code that will appear at the start of the monomorphized function. For
+ // example:
+ //
+ // (call $foo
+ // (i32.const 10)
+ // (..something complicated..)
+ // )
+ // (func $foo (param $int i32) (param $complex f64)
+ // ..
+ //
+ // The context operands are
+ //
+ // [
+ // (i32.const 10) ;; Unchanged: this can be pulled into the called
+ // ;; function, and removed from the caller side.
+ // (local.get $0) ;; The complicated child cannot; keep it as a value
+ // ;; sent from the caller, which we will local.get.
+ // ]
+ //
+ // Both the const and the local.get are simply used in the monomorphized
+ // function, like this:
+ //
+ // (func $foo-monomorphized (param $0 ..)
+ // (..local defs..)
+ // ;; Apply the first operand, which was pulled into here.
+ // (local.set $int
+ // (i32.const 10)
+ // )
+ // ;; Read the second, which remains a parameter to the function.
+ // (local.set $complex
+ // (local.get $0)
+ // )
+ // ;; The original body.
+ // ..
+ //
+ // The $int param is no longer a parameter, and it is set in a local at the
+ // top: we have "reverse-inlined" code from the calling function into the
+ // caller, pulling the constant 10 into here. The second parameter cannot be
+ // pulled in, so we must still send it, but we still have a local.set there to
+ // copy it into a local (this does not matter in this case, but does if the
+ // sent value is more refined; always using a local.set is simpler and more
+ // regular).
+ std::vector<Expression*> operands;
+
+ // Whether the call is dropped. TODO
+ bool dropped;
+
+ bool operator==(const CallContext& other) const {
+ if (dropped != other.dropped) {
+ return false;
+ }
+
+ // We consider logically equivalent expressions as equal (rather than raw
+ // pointers), so that contexts with functionally identical shape are
+ // treated the same.
+ if (operands.size() != other.operands.size()) {
+ return false;
+ }
+ for (Index i = 0; i < operands.size(); i++) {
+ if (!ExpressionAnalyzer::equal(operands[i], other.operands[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool operator!=(const CallContext& other) const { return !(*this == other); }
+
+ // Build the context from a given call. This builds up the context operands as
+ // as explained in the comments above, and updates the call to send any
+ // remaining values by updating |newOperands| (for example, if all the values
+ // sent are constants, then |newOperands| will end up empty, as we have
+ // nothing left to send).
+ void buildFromCall(Call* call,
+ std::vector<Expression*>& newOperands,
+ Module& wasm) {
+ Builder builder(wasm);
+
+ for (auto* operand : call->operands) {
+ // Process the operand. This is a copy operation, as we are trying to move
+ // (copy) code from the callsite into the called function. When we find we
+ // can copy then we do so, and when we cannot that value remains as a
+ // value sent from the call.
+ operands.push_back(ExpressionManipulator::flexibleCopy(
+ operand, wasm, [&](Expression* child) -> Expression* {
+ if (canBeMovedIntoContext(child)) {
+ // This can be moved, great: let the copy happen.
+ return nullptr;
+ }
+
+ // This cannot be moved, so we stop here: this is a value that is sent
+ // into the monomorphized function. It is a new operand in the call,
+ // and in the context operands it is a local.get, that reads that
+ // value.
+ auto paramIndex = newOperands.size();
+ newOperands.push_back(child);
+ // TODO: If one operand is a tee and another a get, we could actually
+ // reuse the local, effectively showing the monomorphized
+ // function that the values are the same. (But then the checks
+ // later down to is<LocalGet> would need to check index too.)
+ return builder.makeLocalGet(paramIndex, child->type);
+ }));
+ }
+
+ // TODO: handle drop
+ dropped = false;
+ }
+
+ // Checks whether an expression can be moved into the context.
+ bool canBeMovedIntoContext(Expression* curr) {
+ // Constant numbers, funcs, strings, etc. can all be copied, so it is ok to
+ // add them to the context.
+ // TODO: Allow global.get as well, and anything else that is purely
+ // copyable.
+ return Properties::isSingleConstantExpression(curr);
+ }
+
+ // Check if a context is trivial relative to a call, that is, the context
+ // contains no information that can allow optimization at all. Such trivial
+ // contexts can be dismissed early.
+ bool isTrivial(Call* call, Module& wasm) {
+ // Dropped contexts are not trivial.
+ if (dropped) {
+ return false;
+ }
+
+ // The context must match the call for us to compare them.
+ assert(operands.size() == call->operands.size());
+
+ // If an operand is not simply passed through, then we are not trivial.
+ auto callParams = wasm.getFunction(call->target)->getParams();
+ for (Index i = 0; i < operands.size(); i++) {
+ // A local.get of the same type implies we just pass through the value.
+ // Anything else is not trivial.
+ if (!operands[i]->is<LocalGet>() || operands[i]->type != callParams[i]) {
+ return false;
+ }
+ }
+
+ // We found nothing interesting, so this is trivial.
+ return true;
+ }
+};
+
+} // anonymous namespace
+
+} // namespace wasm
+
+namespace std {
+
+template<> struct hash<wasm::CallContext> {
+ size_t operator()(const wasm::CallContext& info) const {
+ size_t digest = hash<bool>{}(info.dropped);
+
+ wasm::rehash(digest, info.operands.size());
+ for (auto* operand : info.operands) {
+ wasm::hash_combine(digest, wasm::ExpressionAnalyzer::hash(operand));
+ }
+
+ return digest;
+ }
+};
+
+// Useful for debugging.
+[[maybe_unused]] void dump(std::ostream& o, wasm::CallContext& context) {
+ o << "CallContext{\n";
+ for (auto* operand : context.operands) {
+ o << " " << *operand << '\n';
+ }
+ if (context.dropped) {
+ o << " dropped\n";
+ }
+ o << "}\n";
+}
+
+} // namespace std
+
+namespace wasm {
+
+namespace {
+
struct Monomorphize : public Pass {
- // If set, we run some opts to see if monomorphization helps, and skip it if
- // not.
+ // If set, we run some opts to see if monomorphization helps, and skip cases
+ // where we do not help out.
bool onlyWhenHelpful;
Monomorphize(bool onlyWhenHelpful) : onlyWhenHelpful(onlyWhenHelpful) {}
void run(Module* module) override {
- if (!module->features.hasGC()) {
- return;
- }
-
// TODO: parallelize, see comments below
// Note the list of all functions. We'll be adding more, and do not want to
@@ -94,7 +306,7 @@ struct Monomorphize : public Pass {
*module, [&](Function* func) { funcNames.push_back(func->name); });
// Find the calls in each function and optimize where we can, changing them
- // to call more refined targets.
+ // to call the monomorphized targets.
for (auto name : funcNames) {
auto* func = module->getFunction(name);
for (auto* call : FindAll<Call>(func->body).list) {
@@ -110,59 +322,67 @@ struct Monomorphize : public Pass {
continue;
}
- call->target = getRefinedTarget(call, module);
+ processCall(call, *module);
}
}
}
- // Given a call, make a copy of the function it is calling that has more
- // refined arguments that fit the arguments being passed perfectly.
- Name getRefinedTarget(Call* call, Module* module) {
+ // Try to optimize a call.
+ void processCall(Call* call, Module& wasm) {
auto target = call->target;
- auto* func = module->getFunction(target);
+ auto* func = wasm.getFunction(target);
if (func->imported()) {
// Nothing to do since this calls outside of the module.
- return target;
- }
- auto params = func->getParams();
- bool hasRefinedParam = false;
- for (Index i = 0; i < call->operands.size(); i++) {
- if (call->operands[i]->type != params[i]) {
- hasRefinedParam = true;
- break;
- }
- }
- if (!hasRefinedParam) {
- // Nothing to do since all params are fully refined already.
- return target;
+ return;
}
- std::vector<Type> refinedTypes;
- for (auto* operand : call->operands) {
- refinedTypes.push_back(operand->type);
- }
- auto refinedParams = Type(refinedTypes);
- auto iter = funcParamMap.find({target, refinedParams});
- if (iter != funcParamMap.end()) {
- return iter->second;
+ // TODO: ignore calls with unreachable operands for simplicty
+
+ // Compute the call context, and the new operands that the call would send
+ // if we use that context.
+ CallContext context;
+ std::vector<Expression*> newOperands;
+ context.buildFromCall(call, newOperands, wasm);
+
+ // See if we've already evaluated this function + call context. If so, then
+ // we've memoized the result.
+ auto iter = funcContextMap.find({target, context});
+ if (iter != funcContextMap.end()) {
+ auto newTarget = iter->second;
+ if (newTarget != target) {
+ // When we computed this before we found a benefit to optimizing, and
+ // created a new monomorphized function to call. Use it by simply
+ // applying the new operands we computed, and adjusting the call target.
+ call->operands.set(newOperands);
+ call->target = newTarget;
+ }
+ return;
}
- // This is the first time we see this situation. Let's see if it is worth
- // monomorphizing.
+ // This is the first time we see this situation. First, check if the context
+ // is trivial and has no opportunities for optimization.
+ if (context.isTrivial(call, wasm)) {
+ // Memoize the failure, and stop.
+ funcContextMap[{target, context}] = target;
+ return;
+ }
- // Create a new function with refined parameters as a copy of the original.
- auto refinedTarget = Names::getValidFunctionName(*module, target);
- auto* refinedFunc = ModuleUtils::copyFunction(func, *module, refinedTarget);
- TypeUpdating::updateParamTypes(refinedFunc, refinedTypes, *module);
- refinedFunc->type = HeapType(Signature(refinedParams, func->getResults()));
+ // Create the monomorphized function that includes the call context.
+ std::unique_ptr<Function> monoFunc =
+ makeMonoFunctionWithContext(func, context, wasm);
- // Assume we'll choose to use the refined target, but if we are being
- // careful then we might change our mind.
- auto chosenTarget = refinedTarget;
+ // Decide whether it is worth using the monomorphized function.
+ auto worthwhile = true;
if (onlyWhenHelpful) {
// Optimize both functions using minimal opts, hopefully enough to see if
- // there is a benefit to the refined types (such as the new types allowing
- // a cast to be removed).
+ // there is a benefit to the context. We optimize both to avoid confusion
+ // from the function benefiting from simply running another cycle of
+ // optimization.
+ //
+ // Note that we do *not* discard the optimizations to the original
+ // function if we decide not to optimize. We've already done them, and the
+ // function is improved, so we may as well keep them.
+ //
// TODO: Atm this can be done many times per function as it is once per
// function and per set of types sent to it. Perhaps have some
// total limit to avoid slow runtimes.
@@ -181,23 +401,155 @@ struct Monomorphize : public Pass {
// keep optimizing from the current contents as we go. It's not
// obvious which approach is best here.
doMinimalOpts(func);
- doMinimalOpts(refinedFunc);
+ doMinimalOpts(monoFunc.get());
auto costBefore = CostAnalyzer(func->body).cost;
- auto costAfter = CostAnalyzer(refinedFunc->body).cost;
+ auto costAfter = CostAnalyzer(monoFunc->body).cost;
+ // TODO: We should probably only accept improvements above some minimum,
+ // to avoid optimizing cases where we duplicate a huge function but
+ // only optimize a tiny part of it compared to the original.
if (costAfter >= costBefore) {
- // We failed to improve. Remove the new function and return the old
- // target.
- module->removeFunction(refinedTarget);
- chosenTarget = target;
+ worthwhile = false;
}
}
- // Mark the chosen target in the map, so we don't do this work again: every
- // pair of target and refinedParams is only considered once.
- funcParamMap[{target, refinedParams}] = chosenTarget;
+ // Memoize what we decided to call here.
+ funcContextMap[{target, context}] = worthwhile ? monoFunc->name : target;
+
+ if (worthwhile) {
+ // We are using the monomorphized function, so update the call and add it
+ // to the module.
+ call->operands.set(newOperands);
+ call->target = monoFunc->name;
+
+ wasm.addFunction(std::move(monoFunc));
+ }
+ }
+
+ // Create a monomorphized function from the original + the call context. It
+ // may have different parameters, results, and may include parts of the call
+ // context.
+ std::unique_ptr<Function> makeMonoFunctionWithContext(
+ Function* func, const CallContext& context, Module& wasm) {
+
+ // The context has an operand for each one in the old function, each of
+ // which may contain reverse-inlined content. A mismatch here means we did
+ // not build the context right, or are using it with the wrong function.
+ assert(context.operands.size() == func->getNumParams());
+
+ // Pick a new name.
+ auto newName = Names::getValidFunctionName(wasm, func->name);
+
+ // Copy the function as the base for the new one.
+ auto newFunc = ModuleUtils::copyFunctionWithoutAdd(func, wasm, newName);
+
+ // Generate the new signature, and apply it to the new function.
+ std::vector<Type> newParams;
+ for (auto* operand : context.operands) {
+ // A local.get is a value that arrives in a parameter. Anything else is
+ // something that we are reverse-inlining into the function, so we don't
+ // need a param for it.
+ if (operand->is<LocalGet>()) {
+ newParams.push_back(operand->type);
+ }
+ }
+ // TODO: support changes to results.
+ auto newResults = func->getResults();
+ newFunc->type = Signature(Type(newParams), newResults);
+
+ // We must update local indexes: the new function has a potentially
+ // different number of parameters, and parameters are at the very bottom of
+ // the local index space. We are also replacing old params with vars. To
+ // track this, map each old index to the new one.
+ std::unordered_map<Index, Index> mappedLocals;
+ auto newParamsMinusOld =
+ newFunc->getParams().size() - func->getParams().size();
+ for (Index i = 0; i < func->getNumLocals(); i++) {
+ if (func->isParam(i)) {
+ // Old params become new vars inside the function. Below we'll copy the
+ // proper values into these vars.
+ // TODO: We could avoid a var + copy when it is trivial (atm we rely on
+ // optimizations to remove it).
+ auto local = Builder::addVar(newFunc.get(), func->getLocalType(i));
+ mappedLocals[i] = local;
+ } else {
+ // This is a var. The only thing to adjust here is that the parameters
+ // are changing.
+ mappedLocals[i] = i + newParamsMinusOld;
+ }
+ }
+
+ // Copy over local names to help debugging.
+ if (!func->localNames.empty()) {
+ newFunc->localNames.clear();
+ newFunc->localIndices.clear();
+ for (Index i = 0; i < func->getNumLocals(); i++) {
+ auto oldName = func->getLocalNameOrDefault(i);
+ if (oldName.isNull()) {
+ continue;
+ }
+
+ auto newIndex = mappedLocals[i];
+ auto newName = Names::getValidLocalName(*newFunc.get(), oldName);
+ newFunc->localNames[newIndex] = newName;
+ newFunc->localIndices[newName] = newIndex;
+ }
+ };
+
+ Builder builder(wasm);
+
+ // Surrounding the main body is the reverse-inlined content from the call
+ // context, like this:
+ //
+ // (func $monomorphized
+ // (..reverse-inlined parameter..)
+ // (..old body..)
+ // )
+ //
+ // For example, if a function that simply returns its input is called with a
+ // constant parameter, it will end up like this:
+ //
+ // (func $monomorphized
+ // (local $param i32)
+ // (local.set $param (i32.const 42)) ;; reverse-inlined parameter
+ // (local.get $param) ;; copied old body
+ // )
+ //
+ // We need to add such an local.set in the prelude of the function for each
+ // operand in the context.
+ std::vector<Expression*> pre;
+ for (Index i = 0; i < context.operands.size(); i++) {
+ auto* operand = context.operands[i];
+
+ // Write the context operand (the reverse-inlined content) to the local
+ // we've allocated for this.
+ auto local = mappedLocals.at(i);
+ auto* value = ExpressionManipulator::copy(operand, wasm);
+ pre.push_back(builder.makeLocalSet(local, value));
+ }
+
+ // Map locals.
+ struct LocalUpdater : public PostWalker<LocalUpdater> {
+ const std::unordered_map<Index, Index>& mappedLocals;
+ LocalUpdater(const std::unordered_map<Index, Index>& mappedLocals)
+ : mappedLocals(mappedLocals) {}
+ void visitLocalGet(LocalGet* curr) { updateIndex(curr->index); }
+ void visitLocalSet(LocalSet* curr) { updateIndex(curr->index); }
+ void updateIndex(Index& index) {
+ auto iter = mappedLocals.find(index);
+ assert(iter != mappedLocals.end());
+ index = iter->second;
+ }
+ } localUpdater(mappedLocals);
+ localUpdater.walk(newFunc->body);
+
+ if (!pre.empty()) {
+ // Add the block after the prelude.
+ pre.push_back(newFunc->body);
+ newFunc->body = builder.makeBlock(pre);
+ }
- return chosenTarget;
+ return newFunc;
}
// Run minimal function-level optimizations on a function. This optimizes at
@@ -219,19 +571,20 @@ struct Monomorphize : public Pass {
// the entire point is that parameters now have more refined types, which
// can lead to locals reading them being refinable as well.
runner.add("local-subtyping");
+ // TODO: we need local propagation and escape analysis etc. -O3?
runner.addDefaultFunctionOptimizationPasses();
runner.setIsNested(true);
runner.runOnFunction(func);
}
- // Maps [func name, param types] to the name of a new function whose params
- // have those types.
+ // Maps [func name, call info] to the name of a new function which is a
+ // monomorphization of that function, specialized to that call info.
//
- // Note that this can contain funcParamMap{A, types} = A, that is, that maps
+ // Note that this can contain funcContextMap{A, ...} = A, that is, that maps
// a function name to itself. That indicates we found no benefit from
- // refining with those particular types, and saves us from computing it again
+ // monomorphizing with that context, and saves us from computing it again
// later on.
- std::unordered_map<std::pair<Name, Type>, Name> funcParamMap;
+ std::unordered_map<std::pair<Name, CallContext>, Name> funcContextMap;
};
} // anonymous namespace