| /* | |
| * Copyright 2016 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. | |
| */ | |
| #ifndef wasm_tools_optimization_options_h | |
| #define wasm_tools_optimization_options_h | |
| #include "tool-options.h" | |
| // | |
| // Shared optimization options for commandline tools | |
| // | |
| namespace wasm { | |
| struct OptimizationOptions : public ToolOptions { | |
| // By default we allow StackIR and enable it by default in higher optimization | |
| // levels, but users can disallow it as well. | |
| bool allowStackIR = true; | |
| void parse(int argc, const char* argv[]) { | |
| ToolOptions::parse(argc, argv); | |
| // After parsing the arguments, update defaults based on the optimize/shrink | |
| // levels. | |
| if (allowStackIR && | |
| (passOptions.optimizeLevel >= 2 || passOptions.shrinkLevel >= 1)) { | |
| passOptions.generateStackIR = true; | |
| passOptions.optimizeStackIR = true; | |
| } | |
| } | |
| static constexpr const char* DEFAULT_OPT_PASSES = "O"; | |
| static constexpr const int OS_OPTIMIZE_LEVEL = 2; | |
| static constexpr const int OS_SHRINK_LEVEL = 1; | |
| // Information to run a pass, as requested by a commandline flag. | |
| struct PassInfo { | |
| // The name of the pass to run. | |
| std::string name; | |
| // The main argument of the pass, if applicable. | |
| std::optional<std::string> argument; | |
| // The optimize and shrink levels to run the pass with, if specified. If not | |
| // specified then the defaults are used. | |
| std::optional<int> optimizeLevel; | |
| std::optional<int> shrinkLevel; | |
| PassInfo(std::string name) : name(name) {} | |
| PassInfo(const char* name) : name(name) {} | |
| PassInfo(std::string name, int optimizeLevel, int shrinkLevel) | |
| : name(name), optimizeLevel(optimizeLevel), shrinkLevel(shrinkLevel) {} | |
| }; | |
| std::vector<PassInfo> passes; | |
| // Normally we assume no wasm-opt or other toolchain opts happen after us, | |
| // but the user can tell us otherwise, which inhibits some opts (ones that | |
| // limit subsequent opts). | |
| bool expectLaterToolchainOpts = false; | |
| // Add a request to run all the default opt passes. They are run with the | |
| // current opt and shrink levels specified, which are read from passOptions. | |
| // | |
| // Each caller to here sets the opt and shrink levels before, which provides | |
| // the right values for us to read. That is, -Os etc. sets the default opt | |
| // level, so that the last of -O3 -Os will override the previous default, but | |
| // also we note the current opt level for when we run the pass, so that the | |
| // sequence -O3 -Os will run -O3 and then -Os, and not -Os twice. | |
| void addDefaultOptPasses() { | |
| passes.push_back(PassInfo{ | |
| DEFAULT_OPT_PASSES, passOptions.optimizeLevel, passOptions.shrinkLevel}); | |
| } | |
| constexpr static const char* OptimizationOptionsCategory = | |
| "Optimization options"; | |
| OptimizationOptions(const std::string& command, | |
| const std::string& description) | |
| : ToolOptions(command, description) { | |
| (*this) | |
| .add( | |
| "", | |
| "-O", | |
| "execute default optimization passes (equivalent to -Os)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.setDefaultOptimizationOptions(); | |
| static_assert( | |
| PassOptions::DEFAULT_OPTIMIZE_LEVEL == OS_OPTIMIZE_LEVEL && | |
| PassOptions::DEFAULT_SHRINK_LEVEL == OS_SHRINK_LEVEL, | |
| "Help text states that -O is equivalent to -Os but now it isn't."); | |
| addDefaultOptPasses(); | |
| }) | |
| .add("", | |
| "-O0", | |
| "execute no optimization passes", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.optimizeLevel = 0; | |
| passOptions.shrinkLevel = 0; | |
| }) | |
| .add("", | |
| "-O1", | |
| "execute -O1 optimization passes (quick&useful opts, useful for " | |
| "iteration builds)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.optimizeLevel = 1; | |
| passOptions.shrinkLevel = 0; | |
| addDefaultOptPasses(); | |
| }) | |
| .add( | |
| "", | |
| "-O2", | |
| "execute -O2 optimization passes (most opts, generally gets most perf)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.optimizeLevel = 2; | |
| passOptions.shrinkLevel = 0; | |
| addDefaultOptPasses(); | |
| }) | |
| .add("", | |
| "-O3", | |
| "execute -O3 optimization passes (spends potentially a lot of time " | |
| "optimizing)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.optimizeLevel = 3; | |
| passOptions.shrinkLevel = 0; | |
| addDefaultOptPasses(); | |
| }) | |
| .add("", | |
| "-O4", | |
| "execute -O4 optimization passes (also flatten the IR, which can " | |
| "take a lot more time and memory, but is useful on more nested / " | |
| "complex / less-optimized input)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.optimizeLevel = 4; | |
| passOptions.shrinkLevel = 0; | |
| addDefaultOptPasses(); | |
| }) | |
| .add("", | |
| "-Os", | |
| "execute default optimization passes, focusing on code size", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.optimizeLevel = OS_OPTIMIZE_LEVEL; | |
| passOptions.shrinkLevel = OS_SHRINK_LEVEL; | |
| addDefaultOptPasses(); | |
| }) | |
| .add("", | |
| "-Oz", | |
| "execute default optimization passes, super-focusing on code size", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.optimizeLevel = 2; | |
| passOptions.shrinkLevel = 2; | |
| addDefaultOptPasses(); | |
| }) | |
| .add("--optimize-level", | |
| "-ol", | |
| "How much to focus on optimizing code", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::One, | |
| [this](Options* o, const std::string& argument) { | |
| passOptions.optimizeLevel = atoi(argument.c_str()); | |
| }) | |
| .add("--shrink-level", | |
| "-s", | |
| "How much to focus on shrinking code size", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::One, | |
| [this](Options* o, const std::string& argument) { | |
| passOptions.shrinkLevel = atoi(argument.c_str()); | |
| }) | |
| .add("--debuginfo", | |
| "-g", | |
| "Emit names section in wasm binary (or full debuginfo in wast)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [&](Options* o, const std::string& arguments) { | |
| passOptions.debugInfo = true; | |
| }) | |
| .add("--no-stack-ir", | |
| "", | |
| "do not use StackIR (even when it is the default)", | |
| ToolOptionsCategory, | |
| Options::Arguments::Zero, | |
| [&](Options* o, const std::string& arguments) { | |
| allowStackIR = false; | |
| passOptions.generateStackIR = false; | |
| passOptions.optimizeStackIR = false; | |
| }) | |
| .add("--always-inline-max-function-size", | |
| "-aimfs", | |
| "Max size of functions that are always inlined (default " + | |
| std::to_string(InliningOptions().alwaysInlineMaxSize) + | |
| ", which " | |
| "is safe for use with -Os builds)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::One, | |
| [this](Options* o, const std::string& argument) { | |
| passOptions.inlining.alwaysInlineMaxSize = | |
| static_cast<Index>(atoi(argument.c_str())); | |
| }) | |
| .add("--flexible-inline-max-function-size", | |
| "-fimfs", | |
| "Max size of functions that are inlined when lightweight (no loops " | |
| "or function calls) when optimizing aggressively for speed (-O3). " | |
| "Default: " + | |
| std::to_string(InliningOptions().flexibleInlineMaxSize), | |
| OptimizationOptionsCategory, | |
| Options::Arguments::One, | |
| [this](Options* o, const std::string& argument) { | |
| passOptions.inlining.flexibleInlineMaxSize = | |
| static_cast<Index>(atoi(argument.c_str())); | |
| }) | |
| .add("--one-caller-inline-max-function-size", | |
| "-ocimfs", | |
| "Max size of functions that are inlined when there is only one " | |
| "caller (default -1, which means all such functions are inlined)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::One, | |
| [this](Options* o, const std::string& argument) { | |
| static_assert(InliningOptions().oneCallerInlineMaxSize == | |
| Index(-1), | |
| "the help text here is written to assume -1"); | |
| passOptions.inlining.oneCallerInlineMaxSize = | |
| static_cast<Index>(atoi(argument.c_str())); | |
| }) | |
| .add("--inline-max-combined-binary-size", | |
| "-imcbs", | |
| "Max size of combined functions after inlining. " | |
| "Default: " + | |
| std::to_string(InliningOptions().maxCombinedBinarySize), | |
| OptimizationOptionsCategory, | |
| Options::Arguments::One, | |
| [this](Options* o, const std::string& argument) { | |
| passOptions.inlining.maxCombinedBinarySize = | |
| static_cast<Index>(atoi(argument.c_str())); | |
| }) | |
| .add("--inline-functions-with-loops", | |
| "-ifwl", | |
| "Allow inlining functions with loops", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options* o, const std::string&) { | |
| passOptions.inlining.allowFunctionsWithLoops = true; | |
| }) | |
| .add("--partial-inlining-ifs", | |
| "-pii", | |
| "Number of ifs allowed in partial inlining (zero means partial " | |
| "inlining is disabled) (default: " + | |
| std::to_string(InliningOptions().partialInliningIfs) + ')', | |
| OptimizationOptionsCategory, | |
| Options::Arguments::One, | |
| [this](Options* o, const std::string& argument) { | |
| passOptions.inlining.partialInliningIfs = | |
| static_cast<Index>(std::stoi(argument)); | |
| }) | |
| .add("--ignore-implicit-traps", | |
| "-iit", | |
| "Optimize under the helpful assumption that no surprising traps " | |
| "occur (from load, div/mod, etc.)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.ignoreImplicitTraps = true; | |
| }) | |
| .add("--traps-never-happen", | |
| "-tnh", | |
| "Optimize under the helpful assumption that no trap is reached at " | |
| "runtime (from load, div/mod, etc.)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.trapsNeverHappen = true; | |
| }) | |
| .add("--low-memory-unused", | |
| "-lmu", | |
| "Optimize under the helpful assumption that the low 1K of memory is " | |
| "not used by the application", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.lowMemoryUnused = true; | |
| }) | |
| .add( | |
| "--fast-math", | |
| "-ffm", | |
| "Optimize floats without handling corner cases of NaNs, rounding, and " | |
| "negative zero", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { passOptions.fastMath = true; }) | |
| .add("--zero-filled-memory", | |
| "-uim", | |
| "Assume that an imported memory will be zero-initialized", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| passOptions.zeroFilledMemory = true; | |
| }) | |
| .add("--expect-later-toolchain-opts", | |
| "", | |
| "Expect further invocations of wasm-opt later, avoiding " | |
| "optimizations that might inhibit them", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::Zero, | |
| [this](Options*, const std::string&) { | |
| expectLaterToolchainOpts = true; | |
| }) | |
| .add("--skip-pass", | |
| "-sp", | |
| "Skip a pass (do not run it)", | |
| OptimizationOptionsCategory, | |
| Options::Arguments::N, | |
| [this](Options*, const std::string& pass) { | |
| passOptions.passesToSkip.insert(pass); | |
| }); | |
| // add passes in registry | |
| for (const auto& p : PassRegistry::get()->getRegisteredNames()) { | |
| (*this).add( | |
| std::string("--") + p, | |
| "", | |
| PassRegistry::get()->getPassDescription(p), | |
| "Optimization passes", | |
| // Allow an optional parameter to a pass. If provided, it is | |
| // the same as if using --pass-arg, that is, | |
| // | |
| // --foo=ARG | |
| // | |
| // is the same as | |
| // | |
| // --foo --pass-arg=foo@ARG | |
| Options::Arguments::Optional, | |
| [this, p](Options*, const std::string& arg) { | |
| PassInfo info(p); | |
| if (!arg.empty()) { | |
| info.argument = arg; | |
| } | |
| passes.push_back(info); | |
| }, | |
| PassRegistry::get()->isPassHidden(p)); | |
| } | |
| } | |
| // Pass arguments with the same name as the pass are stored per-instance on | |
| // PassInfo, while all other arguments are stored globally on | |
| // passOptions.arguments (which is what the overridden method on ToolOptions | |
| // does). | |
| void addPassArg(const std::string& key, const std::string& value) override { | |
| // Scan the current pass list for the last defined instance of a pass named | |
| // like the argument under consideration. | |
| for (auto i = passes.rbegin(); i != passes.rend(); i++) { | |
| if (i->name != key) { | |
| continue; | |
| } | |
| if (i->argument.has_value()) { | |
| Fatal() << i->name << " already set to " << *(i->argument); | |
| } | |
| // Found? Store the argument value there and return. | |
| i->argument = value; | |
| return; | |
| } | |
| // Not found? Store it globally if there is no pass with the same name. | |
| if (!PassRegistry::get()->containsPass(key)) { | |
| return ToolOptions::addPassArg(key, value); | |
| } | |
| // Not found, but we have a pass with the same name? Bail out. | |
| Fatal() << "can't set " << key << ": pass not enabled"; | |
| } | |
| bool runningDefaultOptimizationPasses() { | |
| for (auto& pass : passes) { | |
| if (pass.name == DEFAULT_OPT_PASSES) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool runningPasses() { return passes.size() > 0; } | |
| void runPasses(Module& wasm) { | |
| PassRunner passRunner(&wasm, passOptions); | |
| if (debug) { | |
| passRunner.setDebug(true); | |
| } | |
| // Flush anything in the current pass runner, and then reset it to a fresh | |
| // state so it is ready for new things. | |
| auto flush = [&]() { | |
| passRunner.run(); | |
| passRunner.clear(); | |
| }; | |
| // If we do not expect later toolchain opts, then this invocation of | |
| // wasm-opt is the last one, and the last invocation of the default opts is | |
| // the last one. Find that invocation and mark lastOpts=true there, which | |
| // will then remain true until the end. | |
| // | |
| // That is, imagine we are called with this: | |
| // | |
| // wasm-opt --dce -O3 --rse -Oz --licm | |
| // ^^ | |
| // Then lastOpts is set here --^^ | |
| // And it applies from then on: [.......] | |
| // That is, we mark lastOpts on the -Oz (after --dce -O3 --rse) and it | |
| // applies to the -Oz and the --licm. | |
| // | |
| // The idea is that the last -O3/-Oz/etc. is the last big cycle of opts, and | |
| // we can assume it is the last (and other specific passes are assumed to be | |
| // "last" if they are after it). | |
| // | |
| // TODO: We could maybe add a --begin-last-opts flag (whose position on the | |
| // commandline matters) if we needed more fine-grained control here. | |
| passRunner.options.lastOpts = false; | |
| Index lastDefaultOptsIndex = Index(-1); | |
| if (!expectLaterToolchainOpts) { | |
| for (Index i = 0; i < passes.size(); ++i) { | |
| if (passes[i].name == DEFAULT_OPT_PASSES) { | |
| lastDefaultOptsIndex = i; | |
| } | |
| } | |
| if (lastDefaultOptsIndex == Index(-1)) { | |
| // No default opts run at all. Just mark lastOpts now, so it applies to | |
| // the non-default opts (same as it would apply to all non-default opts | |
| // after the last default ones). | |
| passRunner.options.lastOpts = true; | |
| } | |
| } | |
| for (Index i = 0; i < passes.size(); ++i) { | |
| auto& pass = passes[i]; | |
| if (pass.name == DEFAULT_OPT_PASSES) { | |
| // This is something like -O3 or -Oz. We must run this now, in order to | |
| // set the proper opt and shrink levels. To do that, first reset the | |
| // runner so that anything already queued is run (since we can only run | |
| // after those things). | |
| flush(); | |
| // -O3/-Oz etc. always set their own optimize/shrinkLevels. | |
| assert(pass.optimizeLevel); | |
| assert(pass.shrinkLevel); | |
| // Temporarily override the default levels. | |
| assert(passRunner.options.optimizeLevel == passOptions.optimizeLevel); | |
| assert(passRunner.options.shrinkLevel == passOptions.shrinkLevel); | |
| passRunner.options.optimizeLevel = *pass.optimizeLevel; | |
| passRunner.options.shrinkLevel = *pass.shrinkLevel; | |
| // If these are the last default opts, mark that. | |
| if (i == lastDefaultOptsIndex) { | |
| passRunner.options.lastOpts = true; | |
| } | |
| // Run our optimizations now with the custom levels. | |
| passRunner.addDefaultOptimizationPasses(); | |
| flush(); | |
| // Restore the default optimize/shrinkLevels. | |
| passRunner.options.optimizeLevel = passOptions.optimizeLevel; | |
| passRunner.options.shrinkLevel = passOptions.shrinkLevel; | |
| } else { | |
| // This is a normal pass. Add it to the queue for execution. | |
| passRunner.add(pass.name, pass.argument); | |
| // Normal passes do not set their own optimize/shrinkLevels. | |
| assert(!pass.optimizeLevel); | |
| assert(!pass.shrinkLevel); | |
| } | |
| } | |
| flush(); | |
| } | |
| }; | |
| } // namespace wasm | |
| #endif |