/* * Copyright 2020 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "src/sksl/SkSLInliner.h" #ifndef SK_ENABLE_OPTIMIZE_SIZE #include "include/core/SkSpan.h" #include "include/core/SkTypes.h" #include "include/private/base/SkTArray.h" #include "src/base/SkEnumBitMask.h" #include "src/sksl/SkSLAnalysis.h" #include "src/sksl/SkSLDefines.h" #include "src/sksl/SkSLErrorReporter.h" #include "src/sksl/SkSLOperator.h" #include "src/sksl/SkSLPosition.h" #include "src/sksl/analysis/SkSLProgramUsage.h" #include "src/sksl/ir/SkSLBinaryExpression.h" #include "src/sksl/ir/SkSLBreakStatement.h" #include "src/sksl/ir/SkSLChildCall.h" #include "src/sksl/ir/SkSLConstructor.h" #include "src/sksl/ir/SkSLConstructorArray.h" #include "src/sksl/ir/SkSLConstructorArrayCast.h" #include "src/sksl/ir/SkSLConstructorCompound.h" #include "src/sksl/ir/SkSLConstructorCompoundCast.h" #include "src/sksl/ir/SkSLConstructorDiagonalMatrix.h" #include "src/sksl/ir/SkSLConstructorMatrixResize.h" #include "src/sksl/ir/SkSLConstructorScalarCast.h" #include "src/sksl/ir/SkSLConstructorSplat.h" #include "src/sksl/ir/SkSLConstructorStruct.h" #include "src/sksl/ir/SkSLContinueStatement.h" #include "src/sksl/ir/SkSLDiscardStatement.h" #include "src/sksl/ir/SkSLDoStatement.h" #include "src/sksl/ir/SkSLEmptyExpression.h" #include "src/sksl/ir/SkSLExpressionStatement.h" #include "src/sksl/ir/SkSLFieldAccess.h" #include "src/sksl/ir/SkSLForStatement.h" #include "src/sksl/ir/SkSLFunctionCall.h" #include "src/sksl/ir/SkSLFunctionDeclaration.h" #include "src/sksl/ir/SkSLFunctionDefinition.h" #include "src/sksl/ir/SkSLIRNode.h" #include "src/sksl/ir/SkSLIfStatement.h" #include "src/sksl/ir/SkSLIndexExpression.h" #include "src/sksl/ir/SkSLModifierFlags.h" #include "src/sksl/ir/SkSLNop.h" #include "src/sksl/ir/SkSLPostfixExpression.h" #include "src/sksl/ir/SkSLPrefixExpression.h" #include "src/sksl/ir/SkSLProgramElement.h" #include "src/sksl/ir/SkSLReturnStatement.h" #include "src/sksl/ir/SkSLSetting.h" #include "src/sksl/ir/SkSLStatement.h" #include "src/sksl/ir/SkSLSwitchCase.h" #include "src/sksl/ir/SkSLSwitchStatement.h" #include "src/sksl/ir/SkSLSwizzle.h" #include "src/sksl/ir/SkSLSymbolTable.h" #include "src/sksl/ir/SkSLTernaryExpression.h" #include "src/sksl/ir/SkSLType.h" #include "src/sksl/ir/SkSLVarDeclarations.h" #include "src/sksl/ir/SkSLVariable.h" #include "src/sksl/ir/SkSLVariableReference.h" #include "src/sksl/transform/SkSLTransform.h" #include #include #include #include #include #include #include using namespace skia_private; namespace SkSL { namespace { static constexpr int kInlinedStatementLimit = 2500; static bool is_scopeless_block(Statement* stmt) { return stmt->is() && !stmt->as().isScope(); } static std::unique_ptr* find_parent_statement( const std::vector*>& stmtStack) { SkASSERT(!stmtStack.empty()); // Walk the statement stack from back to front, ignoring the last element (which is the // enclosing statement). auto iter = stmtStack.rbegin(); ++iter; // Anything counts as a parent statement other than a scopeless Block. for (; iter != stmtStack.rend(); ++iter) { std::unique_ptr* stmt = *iter; if (!is_scopeless_block(stmt->get())) { return stmt; } } // There wasn't any parent statement to be found. return nullptr; } std::unique_ptr clone_with_ref_kind(const Expression& expr, VariableReference::RefKind refKind, Position pos) { std::unique_ptr clone = expr.clone(pos); Analysis::UpdateVariableRefKind(clone.get(), refKind); return clone; } } // namespace const Variable* Inliner::RemapVariable(const Variable* variable, const VariableRewriteMap* varMap) { std::unique_ptr* remap = varMap->find(variable); if (!remap) { SkDEBUGFAILF("rewrite map does not contain variable '%.*s'", (int)variable->name().size(), variable->name().data()); return variable; } Expression* expr = remap->get(); SkASSERT(expr); if (!expr->is()) { SkDEBUGFAILF("rewrite map contains non-variable replacement for '%.*s'", (int)variable->name().size(), variable->name().data()); return variable; } return expr->as().variable(); } void Inliner::ensureScopedBlocks(Statement* inlinedBody, Statement* parentStmt) { // No changes necessary if this statement isn't actually a block. if (!inlinedBody || !inlinedBody->is()) { return; } // No changes necessary if the parent statement doesn't require a scope. if (!parentStmt || !(parentStmt->is() || parentStmt->is() || parentStmt->is() || is_scopeless_block(parentStmt))) { return; } Block& block = inlinedBody->as(); // The inliner will create inlined function bodies as a Block containing multiple statements, // but no scope. Normally, this is fine, but if this block is used as the statement for a // do/for/if/while, the block needs to be scoped for the generated code to match the intent. // In the case of Blocks nested inside other Blocks, we add the scope to the outermost block if // needed. for (Block* nestedBlock = █; ) { if (nestedBlock->isScope()) { // We found an explicit scope; all is well. return; } if (nestedBlock->children().size() == 1 && nestedBlock->children()[0]->is()) { // This block wraps another unscoped block; we need to go deeper. nestedBlock = &nestedBlock->children()[0]->as(); continue; } // We found a block containing real statements (not just more blocks), but no scope. // Let's add a scope to the outermost block. block.setBlockKind(Block::Kind::kBracedScope); return; } } std::unique_ptr Inliner::inlineExpression(Position pos, VariableRewriteMap* varMap, SymbolTable* symbolTableForExpression, const Expression& expression) { auto expr = [&](const std::unique_ptr& e) -> std::unique_ptr { if (e) { return this->inlineExpression(pos, varMap, symbolTableForExpression, *e); } return nullptr; }; auto argList = [&](const ExpressionArray& originalArgs) -> ExpressionArray { ExpressionArray args; args.reserve_exact(originalArgs.size()); for (const std::unique_ptr& arg : originalArgs) { args.push_back(expr(arg)); } return args; }; auto childRemap = [&](const Variable& var) -> const Variable& { // If our variable remapping table contains the passed-in variable... if (std::unique_ptr* remap = varMap->find(&var)) { // ... the remapped expression _must_ be another variable reference. // SkSL doesn't allow opaque types to participate in complex expressions. if ((*remap)->is()) { const VariableReference& remappedRef = (*remap)->as(); return *remappedRef.variable(); } else { SkDEBUGFAILF("Child effect '%.*s' remaps to unexpected expression '%s'", (int)var.name().size(), var.name().data(), (*remap)->description().c_str()); } } // There's no remapping for this; return it as-is. return var; }; switch (expression.kind()) { case Expression::Kind::kBinary: { const BinaryExpression& binaryExpr = expression.as(); return BinaryExpression::Make(*fContext, pos, expr(binaryExpr.left()), binaryExpr.getOperator(), expr(binaryExpr.right())); } case Expression::Kind::kEmpty: return expression.clone(pos); case Expression::Kind::kLiteral: return expression.clone(pos); case Expression::Kind::kChildCall: { const ChildCall& childCall = expression.as(); return ChildCall::Make(*fContext, pos, childCall.type().clone(*fContext, symbolTableForExpression), childRemap(childCall.child()), argList(childCall.arguments())); } case Expression::Kind::kConstructorArray: { const ConstructorArray& ctor = expression.as(); return ConstructorArray::Make(*fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), argList(ctor.arguments())); } case Expression::Kind::kConstructorArrayCast: { const ConstructorArrayCast& ctor = expression.as(); return ConstructorArrayCast::Make( *fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), expr(ctor.argument())); } case Expression::Kind::kConstructorCompound: { const ConstructorCompound& ctor = expression.as(); return ConstructorCompound::Make( *fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), argList(ctor.arguments())); } case Expression::Kind::kConstructorCompoundCast: { const ConstructorCompoundCast& ctor = expression.as(); return ConstructorCompoundCast::Make( *fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), expr(ctor.argument())); } case Expression::Kind::kConstructorDiagonalMatrix: { const ConstructorDiagonalMatrix& ctor = expression.as(); return ConstructorDiagonalMatrix::Make( *fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), expr(ctor.argument())); } case Expression::Kind::kConstructorMatrixResize: { const ConstructorMatrixResize& ctor = expression.as(); return ConstructorMatrixResize::Make( *fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), expr(ctor.argument())); } case Expression::Kind::kConstructorScalarCast: { const ConstructorScalarCast& ctor = expression.as(); return ConstructorScalarCast::Make( *fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), expr(ctor.argument())); } case Expression::Kind::kConstructorSplat: { const ConstructorSplat& ctor = expression.as(); return ConstructorSplat::Make(*fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), expr(ctor.argument())); } case Expression::Kind::kConstructorStruct: { const ConstructorStruct& ctor = expression.as(); return ConstructorStruct::Make(*fContext, pos, *ctor.type().clone(*fContext, symbolTableForExpression), argList(ctor.arguments())); } case Expression::Kind::kFieldAccess: { const FieldAccess& f = expression.as(); return FieldAccess::Make(*fContext, pos, expr(f.base()), f.fieldIndex(), f.ownerKind()); } case Expression::Kind::kFunctionCall: { const FunctionCall& funcCall = expression.as(); return FunctionCall::Make(*fContext, pos, funcCall.type().clone(*fContext, symbolTableForExpression), funcCall.function(), argList(funcCall.arguments())); } case Expression::Kind::kFunctionReference: return expression.clone(pos); case Expression::Kind::kIndex: { const IndexExpression& idx = expression.as(); return IndexExpression::Make(*fContext, pos, expr(idx.base()), expr(idx.index())); } case Expression::Kind::kMethodReference: return expression.clone(pos); case Expression::Kind::kPrefix: { const PrefixExpression& p = expression.as(); return PrefixExpression::Make(*fContext, pos, p.getOperator(), expr(p.operand())); } case Expression::Kind::kPostfix: { const PostfixExpression& p = expression.as(); return PostfixExpression::Make(*fContext, pos, expr(p.operand()), p.getOperator()); } case Expression::Kind::kSetting: { const Setting& s = expression.as(); return Setting::Make(*fContext, pos, s.capsPtr()); } case Expression::Kind::kSwizzle: { const Swizzle& s = expression.as(); return Swizzle::Make(*fContext, pos, expr(s.base()), s.components()); } case Expression::Kind::kTernary: { const TernaryExpression& t = expression.as(); return TernaryExpression::Make(*fContext, pos, expr(t.test()), expr(t.ifTrue()), expr(t.ifFalse())); } case Expression::Kind::kTypeReference: return expression.clone(pos); case Expression::Kind::kVariableReference: { const VariableReference& v = expression.as(); std::unique_ptr* remap = varMap->find(v.variable()); if (remap) { return clone_with_ref_kind(**remap, v.refKind(), pos); } return expression.clone(pos); } default: SkDEBUGFAILF("unsupported expression: %s", expression.description().c_str()); return nullptr; } } std::unique_ptr Inliner::inlineStatement(Position pos, VariableRewriteMap* varMap, SymbolTable* symbolTableForStatement, std::unique_ptr* resultExpr, Analysis::ReturnComplexity returnComplexity, const Statement& statement, const ProgramUsage& usage, bool isBuiltinCode) { auto stmt = [&](const std::unique_ptr& s) -> std::unique_ptr { if (s) { return this->inlineStatement(pos, varMap, symbolTableForStatement, resultExpr, returnComplexity, *s, usage, isBuiltinCode); } return nullptr; }; auto expr = [&](const std::unique_ptr& e) -> std::unique_ptr { if (e) { return this->inlineExpression(pos, varMap, symbolTableForStatement, *e); } return nullptr; }; auto variableModifiers = [&](const Variable& variable, const Expression* initialValue) -> ModifierFlags { return Transform::AddConstToVarModifiers(variable, initialValue, &usage); }; auto makeWithChildSymbolTable = [&](auto callback) -> std::unique_ptr { SymbolTable* origSymbolTable = symbolTableForStatement; auto childSymbols = std::make_unique(origSymbolTable, isBuiltinCode); symbolTableForStatement = childSymbols.get(); std::unique_ptr stmt = callback(std::move(childSymbols)); symbolTableForStatement = origSymbolTable; return stmt; }; ++fInlinedStatementCounter; switch (statement.kind()) { case Statement::Kind::kBlock: return makeWithChildSymbolTable([&](std::unique_ptr symbolTable) { const Block& block = statement.as(); StatementArray statements; statements.reserve_exact(block.children().size()); for (const std::unique_ptr& child : block.children()) { statements.push_back(stmt(child)); } return Block::Make(pos, std::move(statements), block.blockKind(), std::move(symbolTable)); }); case Statement::Kind::kBreak: return BreakStatement::Make(pos); case Statement::Kind::kContinue: return ContinueStatement::Make(pos); case Statement::Kind::kDiscard: return DiscardStatement::Make(*fContext, pos); case Statement::Kind::kDo: { const DoStatement& d = statement.as(); return DoStatement::Make(*fContext, pos, stmt(d.statement()), expr(d.test())); } case Statement::Kind::kExpression: { const ExpressionStatement& e = statement.as(); return ExpressionStatement::Make(*fContext, expr(e.expression())); } case Statement::Kind::kFor: return makeWithChildSymbolTable([&](std::unique_ptr symbolTable) { const ForStatement& f = statement.as(); // We need to ensure `initializer` is evaluated first, so that we've already // remapped its declaration by the time we evaluate `test` and `next`. std::unique_ptr initializerStmt = stmt(f.initializer()); std::unique_ptr testExpr = expr(f.test()); std::unique_ptr nextExpr = expr(f.next()); std::unique_ptr bodyStmt = stmt(f.statement()); std::unique_ptr unrollInfo; if (f.unrollInfo()) { // The for loop's unroll-info points to the Variable in the initializer as the // index. This variable has been rewritten into a clone by the inliner, so we // need to update the loop-unroll info to point to the clone. unrollInfo = std::make_unique(*f.unrollInfo()); unrollInfo->fIndex = RemapVariable(unrollInfo->fIndex, varMap); } return ForStatement::Make(*fContext, pos, ForLoopPositions{}, std::move(initializerStmt), std::move(testExpr), std::move(nextExpr), std::move(bodyStmt), std::move(unrollInfo), std::move(symbolTable)); }); case Statement::Kind::kIf: { const IfStatement& i = statement.as(); return IfStatement::Make(*fContext, pos, expr(i.test()), stmt(i.ifTrue()), stmt(i.ifFalse())); } case Statement::Kind::kNop: return Nop::Make(); case Statement::Kind::kReturn: { const ReturnStatement& r = statement.as(); if (!r.expression()) { // This function doesn't return a value. We won't inline functions with early // returns, so a return statement is a no-op and can be treated as such. return Nop::Make(); } // If a function only contains a single return, and it doesn't reference variables from // inside an Block's scope, we don't need to store the result in a variable at all. Just // replace the function-call expression with the function's return expression. SkASSERT(resultExpr); if (returnComplexity <= Analysis::ReturnComplexity::kSingleSafeReturn) { *resultExpr = expr(r.expression()); return Nop::Make(); } // For more complex functions, we assign their result into a variable. We refuse to // inline anything with early returns, so this should be safe to do; that is, on this // control path, this is the last statement that will occur. SkASSERT(*resultExpr); return ExpressionStatement::Make( *fContext, BinaryExpression::Make( *fContext, pos, clone_with_ref_kind(**resultExpr, VariableRefKind::kWrite, pos), Operator::Kind::EQ, expr(r.expression()))); } case Statement::Kind::kSwitch: { const SwitchStatement& ss = statement.as(); return SwitchStatement::Make(*fContext, pos, expr(ss.value()), stmt(ss.caseBlock())); } case Statement::Kind::kSwitchCase: { const SwitchCase& sc = statement.as(); return sc.isDefault() ? SwitchCase::MakeDefault(pos, stmt(sc.statement())) : SwitchCase::Make(pos, sc.value(), stmt(sc.statement())); } case Statement::Kind::kVarDeclaration: { const VarDeclaration& decl = statement.as(); std::unique_ptr initialValue = expr(decl.value()); const Variable* variable = decl.var(); // We assign unique names to inlined variables--scopes hide most of the problems in this // regard, but see `InlinerAvoidsVariableNameOverlap` for a counterexample where unique // names are important. const std::string* name = symbolTableForStatement->takeOwnershipOfString( fMangler.uniqueName(variable->name(), symbolTableForStatement)); auto clonedVar = Variable::Make(pos, variable->modifiersPosition(), variable->layout(), variableModifiers(*variable, initialValue.get()), variable->type().clone(*fContext, symbolTableForStatement), name->c_str(), /*mangledName=*/"", isBuiltinCode, variable->storage()); varMap->set(variable, VariableReference::Make(pos, clonedVar.get())); std::unique_ptr result = VarDeclaration::Make(*fContext, clonedVar.get(), decl.baseType().clone(*fContext, symbolTableForStatement), decl.arraySize(), std::move(initialValue)); symbolTableForStatement->add(*fContext, std::move(clonedVar)); return result; } default: SkASSERT(false); return nullptr; } } static bool argument_needs_scratch_variable(const Expression* arg, const Variable* param, const ProgramUsage& usage) { // If the parameter isn't written to within the inline function ... const ProgramUsage::VariableCounts& paramUsage = usage.get(*param); if (!paramUsage.fWrite) { // ... and it can be inlined trivially (e.g. a swizzle, or a constant array index), // or it is any expression without side effects that is only accessed at most once... if ((paramUsage.fRead > 1) ? Analysis::IsTrivialExpression(*arg) : !Analysis::HasSideEffects(*arg)) { // ... we don't need to copy it at all! We can just use the existing expression. return false; } } // We need a scratch variable. return true; } Inliner::InlinedCall Inliner::inlineCall(const FunctionCall& call, SymbolTable* symbolTable, const ProgramUsage& usage, const FunctionDeclaration* caller) { using ScratchVariable = Variable::ScratchVariable; // Inlining is more complicated here than in a typical compiler, because we have to have a // high-level IR and can't just drop statements into the middle of an expression or even use // gotos. // // Since we can't insert statements into an expression, we run the inline function as extra // statements before the statement we're currently processing, relying on a lack of execution // order guarantees. SkASSERT(fContext); SkASSERT(this->isSafeToInline(call.function().definition(), usage)); const ExpressionArray& arguments = call.arguments(); const Position pos = call.fPosition; const FunctionDefinition& function = *call.function().definition(); const Block& body = function.body()->as(); const Analysis::ReturnComplexity returnComplexity = Analysis::GetReturnComplexity(function); StatementArray inlineStatements; int expectedStmtCount = 1 + // Result variable arguments.size() + // Function argument temp-vars body.children().size(); // Inlined code inlineStatements.reserve_exact(expectedStmtCount); std::unique_ptr resultExpr; if (returnComplexity > Analysis::ReturnComplexity::kSingleSafeReturn && !function.declaration().returnType().isVoid()) { // Create a variable to hold the result in the extra statements. We don't need to do this // for void-return functions, or in cases that are simple enough that we can just replace // the function-call node with the result expression. ScratchVariable var = Variable::MakeScratchVariable(*fContext, fMangler, function.declaration().name(), &function.declaration().returnType(), symbolTable, /*initialValue=*/nullptr); inlineStatements.push_back(std::move(var.fVarDecl)); resultExpr = VariableReference::Make(Position(), var.fVarSymbol); } // Create variables in the extra statements to hold the arguments, and assign the arguments to // them. VariableRewriteMap varMap; for (int i = 0; i < arguments.size(); ++i) { const Expression* arg = arguments[i].get(); const Variable* param = function.declaration().parameters()[i]; if (!argument_needs_scratch_variable(arg, param, usage)) { varMap.set(param, arg->clone()); continue; } ScratchVariable var = Variable::MakeScratchVariable(*fContext, fMangler, param->name(), &arg->type(), symbolTable, arg->clone()); inlineStatements.push_back(std::move(var.fVarDecl)); varMap.set(param, VariableReference::Make(Position(), var.fVarSymbol)); } for (const std::unique_ptr& stmt : body.children()) { inlineStatements.push_back(this->inlineStatement(pos, &varMap, symbolTable, &resultExpr, returnComplexity, *stmt, usage, caller->isBuiltin())); } SkASSERT(inlineStatements.size() <= expectedStmtCount); // Wrap all of the generated statements in a block. We need a real Block here, because we need // to add another child statement to the Block later. InlinedCall inlinedCall; inlinedCall.fInlinedBody = Block::MakeBlock(pos, std::move(inlineStatements), Block::Kind::kUnbracedBlock); if (resultExpr) { // Return our result expression as-is. inlinedCall.fReplacementExpr = std::move(resultExpr); } else if (function.declaration().returnType().isVoid()) { // It's a void function, so its result is the empty expression. inlinedCall.fReplacementExpr = EmptyExpression::Make(pos, *fContext); } else { // It's a non-void function, but it never created a result expression--that is, it never // returned anything on any path! This should have been detected in the function finalizer. // Still, discard our output and generate an error. SkDEBUGFAIL("inliner found non-void function that fails to return a value on any path"); fContext->fErrors->error(function.fPosition, "inliner found non-void function '" + std::string(function.declaration().name()) + "' that fails to return a value on any path"); inlinedCall = {}; } return inlinedCall; } bool Inliner::isSafeToInline(const FunctionDefinition* functionDef, const ProgramUsage& usage) { // A threshold of zero indicates that the inliner is completely disabled, so we can just return. if (this->settings().fInlineThreshold <= 0) { return false; } // Enforce a limit on inlining to avoid pathological cases. (inliner/ExponentialGrowth.sksl) if (fInlinedStatementCounter >= kInlinedStatementLimit) { return false; } if (functionDef == nullptr) { // Can't inline something if we don't actually have its definition. return false; } if (functionDef->declaration().modifierFlags().isNoInline()) { // Refuse to inline functions decorated with `noinline`. return false; } for (const Variable* param : functionDef->declaration().parameters()) { // We don't allow inlining functions with parameters that are written-to, if they... // - are `out` parameters (see skia:11326 for rationale.) // - are arrays or structures (introducing temporary copies is non-trivial) if ((param->modifierFlags() & ModifierFlag::kOut) || param->type().isArray() || param->type().isStruct()) { ProgramUsage::VariableCounts counts = usage.get(*param); if (counts.fWrite > 0) { return false; } } } // We don't have a mechanism to simulate early returns, so we can't inline if there is one. return Analysis::GetReturnComplexity(*functionDef) < Analysis::ReturnComplexity::kEarlyReturns; } // A candidate function for inlining, containing everything that `inlineCall` needs. struct InlineCandidate { SymbolTable* fSymbols; // the SymbolTable of the candidate std::unique_ptr* fParentStmt; // the parent Statement of the enclosing stmt std::unique_ptr* fEnclosingStmt; // the Statement containing the candidate std::unique_ptr* fCandidateExpr; // the candidate FunctionCall to be inlined FunctionDefinition* fEnclosingFunction; // the Function containing the candidate }; struct InlineCandidateList { std::vector fCandidates; }; class InlineCandidateAnalyzer { public: // A list of all the inlining candidates we found during analysis. InlineCandidateList* fCandidateList; // A stack of the symbol tables; since most nodes don't have one, expected to be shallower than // the enclosing-statement stack. std::vector fSymbolTableStack; // A stack of "enclosing" statements--these would be suitable for the inliner to use for adding // new instructions. Not all statements are suitable (e.g. a for-loop's initializer). The // inliner might replace a statement with a block containing the statement. std::vector*> fEnclosingStmtStack; // The function that we're currently processing (i.e. inlining into). FunctionDefinition* fEnclosingFunction = nullptr; void visit(const std::vector>& elements, SymbolTable* symbols, InlineCandidateList* candidateList) { fCandidateList = candidateList; fSymbolTableStack.push_back(symbols); for (const std::unique_ptr& pe : elements) { this->visitProgramElement(pe.get()); } fSymbolTableStack.pop_back(); fCandidateList = nullptr; } void visitProgramElement(ProgramElement* pe) { switch (pe->kind()) { case ProgramElement::Kind::kFunction: { FunctionDefinition& funcDef = pe->as(); // If this function has parameter names that would shadow globally-scoped names, we // don't scan it for inline candidates, because it's too late to mangle the names. bool foundShadowingParameterName = false; for (const Variable* param : funcDef.declaration().parameters()) { if (fSymbolTableStack.front()->find(param->name())) { foundShadowingParameterName = true; break; } } if (!foundShadowingParameterName) { fEnclosingFunction = &funcDef; this->visitStatement(&funcDef.body()); } break; } default: // The inliner can't operate outside of a function's scope. break; } } void visitStatement(std::unique_ptr* stmt, bool isViableAsEnclosingStatement = true) { if (!*stmt) { return; } Analysis::SymbolTableStackBuilder scopedStackBuilder(stmt->get(), &fSymbolTableStack); // If this statement contains symbols that would shadow globally-scoped names, we don't look // for any inline candidates, because it's too late to mangle the names. if (scopedStackBuilder.foundSymbolTable() && fSymbolTableStack.back()->wouldShadowSymbolsFrom(fSymbolTableStack.front())) { return; } size_t oldEnclosingStmtStackSize = fEnclosingStmtStack.size(); if (isViableAsEnclosingStatement) { fEnclosingStmtStack.push_back(stmt); } switch ((*stmt)->kind()) { case Statement::Kind::kBreak: case Statement::Kind::kContinue: case Statement::Kind::kDiscard: case Statement::Kind::kNop: break; case Statement::Kind::kBlock: { Block& block = (*stmt)->as(); for (std::unique_ptr& blockStmt : block.children()) { this->visitStatement(&blockStmt); } break; } case Statement::Kind::kDo: { DoStatement& doStmt = (*stmt)->as(); // The loop body is a candidate for inlining. this->visitStatement(&doStmt.statement()); // The inliner isn't smart enough to inline the test-expression for a do-while // loop at this time. There are two limitations: // - We would need to insert the inlined-body block at the very end of the do- // statement's inner fStatement. We don't support that today, but it's doable. // - We cannot inline the test expression if the loop uses `continue` anywhere; that // would skip over the inlined block that evaluates the test expression. There // isn't a good fix for this--any workaround would be more complex than the cost // of a function call. However, loops that don't use `continue` would still be // viable candidates for inlining. break; } case Statement::Kind::kExpression: { ExpressionStatement& expr = (*stmt)->as(); this->visitExpression(&expr.expression()); break; } case Statement::Kind::kFor: { ForStatement& forStmt = (*stmt)->as(); // The initializer and loop body are candidates for inlining. this->visitStatement(&forStmt.initializer(), /*isViableAsEnclosingStatement=*/false); this->visitStatement(&forStmt.statement()); // The inliner isn't smart enough to inline the test- or increment-expressions // of a for loop loop at this time. There are a handful of limitations: // - We would need to insert the test-expression block at the very beginning of the // for-loop's inner fStatement, and the increment-expression block at the very // end. We don't support that today, but it's doable. // - The for-loop's built-in test-expression would need to be dropped entirely, // and the loop would be halted via a break statement at the end of the inlined // test-expression. This is again something we don't support today, but it could // be implemented. // - We cannot inline the increment-expression if the loop uses `continue` anywhere; // that would skip over the inlined block that evaluates the increment expression. // There isn't a good fix for this--any workaround would be more complex than the // cost of a function call. However, loops that don't use `continue` would still // be viable candidates for increment-expression inlining. break; } case Statement::Kind::kIf: { IfStatement& ifStmt = (*stmt)->as(); this->visitExpression(&ifStmt.test()); this->visitStatement(&ifStmt.ifTrue()); this->visitStatement(&ifStmt.ifFalse()); break; } case Statement::Kind::kReturn: { ReturnStatement& returnStmt = (*stmt)->as(); this->visitExpression(&returnStmt.expression()); break; } case Statement::Kind::kSwitch: { SwitchStatement& switchStmt = (*stmt)->as(); this->visitExpression(&switchStmt.value()); for (const std::unique_ptr& switchCase : switchStmt.cases()) { // The switch-case's fValue cannot be a FunctionCall; skip it. this->visitStatement(&switchCase->as().statement()); } break; } case Statement::Kind::kVarDeclaration: { VarDeclaration& varDeclStmt = (*stmt)->as(); // Don't need to scan the declaration's sizes; those are always literals. this->visitExpression(&varDeclStmt.value()); break; } default: SkUNREACHABLE; } // Pop our symbol and enclosing-statement stacks. fEnclosingStmtStack.resize(oldEnclosingStmtStackSize); } void visitExpression(std::unique_ptr* expr) { if (!*expr) { return; } switch ((*expr)->kind()) { case Expression::Kind::kFieldAccess: case Expression::Kind::kFunctionReference: case Expression::Kind::kLiteral: case Expression::Kind::kMethodReference: case Expression::Kind::kSetting: case Expression::Kind::kTypeReference: case Expression::Kind::kVariableReference: // Nothing to scan here. break; case Expression::Kind::kBinary: { BinaryExpression& binaryExpr = (*expr)->as(); this->visitExpression(&binaryExpr.left()); // Logical-and and logical-or binary expressions do not inline the right side, // because that would invalidate short-circuiting. That is, when evaluating // expressions like these: // (false && x()) // always false // (true || y()) // always true // It is illegal for side-effects from x() or y() to occur. The simplest way to // enforce that rule is to avoid inlining the right side entirely. However, it is // safe for other types of binary expression to inline both sides. Operator op = binaryExpr.getOperator(); bool shortCircuitable = (op.kind() == Operator::Kind::LOGICALAND || op.kind() == Operator::Kind::LOGICALOR); if (!shortCircuitable) { this->visitExpression(&binaryExpr.right()); } break; } case Expression::Kind::kChildCall: { ChildCall& childCallExpr = (*expr)->as(); for (std::unique_ptr& arg : childCallExpr.arguments()) { this->visitExpression(&arg); } break; } case Expression::Kind::kConstructorArray: case Expression::Kind::kConstructorArrayCast: case Expression::Kind::kConstructorCompound: case Expression::Kind::kConstructorCompoundCast: case Expression::Kind::kConstructorDiagonalMatrix: case Expression::Kind::kConstructorMatrixResize: case Expression::Kind::kConstructorScalarCast: case Expression::Kind::kConstructorSplat: case Expression::Kind::kConstructorStruct: { AnyConstructor& constructorExpr = (*expr)->asAnyConstructor(); for (std::unique_ptr& arg : constructorExpr.argumentSpan()) { this->visitExpression(&arg); } break; } case Expression::Kind::kFunctionCall: { FunctionCall& funcCallExpr = (*expr)->as(); for (std::unique_ptr& arg : funcCallExpr.arguments()) { this->visitExpression(&arg); } this->addInlineCandidate(expr); break; } case Expression::Kind::kIndex: { IndexExpression& indexExpr = (*expr)->as(); this->visitExpression(&indexExpr.base()); this->visitExpression(&indexExpr.index()); break; } case Expression::Kind::kPostfix: { PostfixExpression& postfixExpr = (*expr)->as(); this->visitExpression(&postfixExpr.operand()); break; } case Expression::Kind::kPrefix: { PrefixExpression& prefixExpr = (*expr)->as(); this->visitExpression(&prefixExpr.operand()); break; } case Expression::Kind::kSwizzle: { Swizzle& swizzleExpr = (*expr)->as(); this->visitExpression(&swizzleExpr.base()); break; } case Expression::Kind::kTernary: { TernaryExpression& ternaryExpr = (*expr)->as(); // The test expression is a candidate for inlining. this->visitExpression(&ternaryExpr.test()); // The true- and false-expressions cannot be inlined, because we are only allowed to // evaluate one side. break; } default: SkUNREACHABLE; } } void addInlineCandidate(std::unique_ptr* candidate) { fCandidateList->fCandidates.push_back( InlineCandidate{fSymbolTableStack.back(), find_parent_statement(fEnclosingStmtStack), fEnclosingStmtStack.back(), candidate, fEnclosingFunction}); } }; static const FunctionDeclaration& candidate_func(const InlineCandidate& candidate) { return (*candidate.fCandidateExpr)->as().function(); } bool Inliner::functionCanBeInlined(const FunctionDeclaration& funcDecl, const ProgramUsage& usage, InlinabilityCache* cache) { if (const bool* cachedInlinability = cache->find(&funcDecl)) { return *cachedInlinability; } bool inlinability = this->isSafeToInline(funcDecl.definition(), usage); cache->set(&funcDecl, inlinability); return inlinability; } bool Inliner::candidateCanBeInlined(const InlineCandidate& candidate, const ProgramUsage& usage, InlinabilityCache* cache) { // Check the cache to see if this function is safe to inline. const FunctionDeclaration& funcDecl = candidate_func(candidate); if (!this->functionCanBeInlined(funcDecl, usage, cache)) { return false; } // Even if the function is safe, the arguments we are passing may not be. In particular, we // can't make copies of opaque values, so we need to reject inline candidates that would need to // do this. Every call has different arguments, so this part is not cacheable. (skia:13824) const FunctionCall& call = candidate.fCandidateExpr->get()->as(); const ExpressionArray& arguments = call.arguments(); for (int i = 0; i < arguments.size(); ++i) { const Expression* arg = arguments[i].get(); if (arg->type().isOpaque()) { const Variable* param = funcDecl.parameters()[i]; if (argument_needs_scratch_variable(arg, param, usage)) { return false; } } } return true; } int Inliner::getFunctionSize(const FunctionDeclaration& funcDecl, FunctionSizeCache* cache) { if (const int* cachedSize = cache->find(&funcDecl)) { return *cachedSize; } int size = Analysis::NodeCountUpToLimit(*funcDecl.definition(), this->settings().fInlineThreshold); cache->set(&funcDecl, size); return size; } void Inliner::buildCandidateList(const std::vector>& elements, SymbolTable* symbols, ProgramUsage* usage, InlineCandidateList* candidateList) { // This is structured much like a ProgramVisitor, but does not actually use ProgramVisitor. // The analyzer needs to keep track of the `unique_ptr*` of statements and expressions so // that they can later be replaced, and ProgramVisitor does not provide this; it only provides a // `const T&`. InlineCandidateAnalyzer analyzer; analyzer.visit(elements, symbols, candidateList); // Early out if there are no inlining candidates. std::vector& candidates = candidateList->fCandidates; if (candidates.empty()) { return; } // Remove candidates that are not safe to inline. InlinabilityCache cache; candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [&](const InlineCandidate& candidate) { return !this->candidateCanBeInlined( candidate, *usage, &cache); }), candidates.end()); // If the inline threshold is unlimited, or if we have no candidates left, our candidate list is // complete. if (this->settings().fInlineThreshold == INT_MAX || candidates.empty()) { return; } // Remove candidates on a per-function basis if the effect of inlining would be to make more // than `inlineThreshold` nodes. (i.e. if Func() would be inlined six times and its size is // 10 nodes, it should be inlined if the inlineThreshold is 60 or higher.) FunctionSizeCache functionSizeCache; FunctionSizeCache candidateTotalCost; for (InlineCandidate& candidate : candidates) { const FunctionDeclaration& fnDecl = candidate_func(candidate); candidateTotalCost[&fnDecl] += this->getFunctionSize(fnDecl, &functionSizeCache); } candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [&](const InlineCandidate& candidate) { const FunctionDeclaration& fnDecl = candidate_func(candidate); if (fnDecl.modifierFlags().isInline()) { // Functions marked `inline` ignore size limitations. return false; } if (usage->get(fnDecl) == 1) { // If a function is only used once, it's cost-free to inline. return false; } if (candidateTotalCost[&fnDecl] <= this->settings().fInlineThreshold) { // We won't exceed the inline threshold by inlining this. return false; } // Inlining this function will add too many IRNodes. return true; }), candidates.end()); } bool Inliner::analyze(const std::vector>& elements, SymbolTable* symbols, ProgramUsage* usage) { // A threshold of zero indicates that the inliner is completely disabled, so we can just return. if (this->settings().fInlineThreshold <= 0) { return false; } // Enforce a limit on inlining to avoid pathological cases. (inliner/ExponentialGrowth.sksl) if (fInlinedStatementCounter >= kInlinedStatementLimit) { return false; } InlineCandidateList candidateList; this->buildCandidateList(elements, symbols, usage, &candidateList); // Inline the candidates where we've determined that it's safe to do so. using StatementRemappingTable = THashMap*, std::unique_ptr*>; StatementRemappingTable statementRemappingTable; bool madeChanges = false; for (const InlineCandidate& candidate : candidateList.fCandidates) { const FunctionCall& funcCall = (*candidate.fCandidateExpr)->as(); // Convert the function call to its inlined equivalent. InlinedCall inlinedCall = this->inlineCall(funcCall, candidate.fSymbols, *usage, &candidate.fEnclosingFunction->declaration()); // Stop if an error was detected during the inlining process. if (!inlinedCall.fInlinedBody && !inlinedCall.fReplacementExpr) { break; } // Ensure that the inlined body has a scope if it needs one. this->ensureScopedBlocks(inlinedCall.fInlinedBody.get(), candidate.fParentStmt->get()); // Add references within the inlined body usage->add(inlinedCall.fInlinedBody.get()); // Look up the enclosing statement; remap it if necessary. std::unique_ptr* enclosingStmt = candidate.fEnclosingStmt; for (;;) { std::unique_ptr** remappedStmt = statementRemappingTable.find(enclosingStmt); if (!remappedStmt) { break; } enclosingStmt = *remappedStmt; } // Move the enclosing statement to the end of the unscoped Block containing the inlined // function, then replace the enclosing statement with that Block. // Before: // fInlinedBody = Block{ stmt1, stmt2, stmt3 } // fEnclosingStmt = stmt4 // After: // fInlinedBody = null // fEnclosingStmt = Block{ stmt1, stmt2, stmt3, stmt4 } inlinedCall.fInlinedBody->children().push_back(std::move(*enclosingStmt)); *enclosingStmt = std::move(inlinedCall.fInlinedBody); // Replace the candidate function call with our replacement expression. usage->remove(candidate.fCandidateExpr->get()); usage->add(inlinedCall.fReplacementExpr.get()); *candidate.fCandidateExpr = std::move(inlinedCall.fReplacementExpr); madeChanges = true; // If anything else pointed at our enclosing statement, it's now pointing at a Block // containing many other statements as well. Maintain a fix-up table to account for this. statementRemappingTable.set(enclosingStmt,&(*enclosingStmt)->as().children().back()); // Stop inlining if we've reached our hard cap on new statements. if (fInlinedStatementCounter >= kInlinedStatementLimit) { break; } // Note that nothing was destroyed except for the FunctionCall. All other nodes should // remain valid. } return madeChanges; } } // namespace SkSL #endif // SK_ENABLE_OPTIMIZE_SIZE