Skip to content

Commit 2d7973a

Browse files
johnstiles-googleSkia Commit-Bot
authored andcommitted
Factor out Inliner candidate list assembly into its own function.
This greatly improves the output from a profiler. It makes it much easier to determine how much time is spent in searching for candidates, versus actually inlining them. It also improves the code readability somewhat by breaking a large monolithic function into several smaller functions. Change-Id: I1b3ef6ddbe46af60e673f37ded766f8077ed6b03 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/321376 Reviewed-by: Ethan Nicholas <ethannicholas@google.com> Commit-Queue: Ethan Nicholas <ethannicholas@google.com> Auto-Submit: John Stiles <johnstiles@google.com>
1 parent 034f78a commit 2d7973a

File tree

5 files changed

+121
-67
lines changed

5 files changed

+121
-67
lines changed

src/sksl/SkSLIRGenerator.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,9 +2116,11 @@ std::unique_ptr<Expression> IRGenerator::call(int offset,
21162116

21172117
auto funcCall = std::make_unique<FunctionCall>(offset, returnType, function,
21182118
std::move(arguments));
2119-
if (fCanInline && fInliner->isSafeToInline(*funcCall, fSettings->fInlineThreshold)) {
2120-
Inliner::InlinedCall inlinedCall =
2121-
fInliner->inlineCall(funcCall.get(), fSymbolTable.get(), fCurrentFunction);
2119+
if (fCanInline &&
2120+
fInliner->isSafeToInline(funcCall->fFunction.fDefinition) &&
2121+
!fInliner->isLargeFunction(funcCall->fFunction.fDefinition)) {
2122+
Inliner::InlinedCall inlinedCall = fInliner->inlineCall(funcCall.get(), fSymbolTable.get(),
2123+
fCurrentFunction);
21222124
if (inlinedCall.fInlinedBody) {
21232125
fExtraStatements.push_back(std::move(inlinedCall.fInlinedBody));
21242126
}

src/sksl/SkSLInliner.cpp

Lines changed: 99 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
#include "src/sksl/SkSLInliner.h"
99

10-
#include "limits.h"
10+
#include <limits.h>
1111
#include <memory>
1212
#include <unordered_set>
1313

@@ -592,7 +592,7 @@ Inliner::InlinedCall Inliner::inlineCall(FunctionCall* call,
592592
SkASSERT(fSettings);
593593
SkASSERT(fContext);
594594
SkASSERT(call);
595-
SkASSERT(this->isSafeToInline(*call, /*inlineThreshold=*/INT_MAX));
595+
SkASSERT(this->isSafeToInline(call->fFunction.fDefinition));
596596

597597
std::vector<std::unique_ptr<Expression>>& arguments = call->fArguments;
598598
const int offset = call->fOffset;
@@ -745,58 +745,52 @@ Inliner::InlinedCall Inliner::inlineCall(FunctionCall* call,
745745
return inlinedCall;
746746
}
747747

748-
bool Inliner::isSafeToInline(const FunctionCall& functionCall, int inlineThreshold) {
748+
bool Inliner::isSafeToInline(const FunctionDefinition* functionDef) {
749749
SkASSERT(fSettings);
750750

751-
if (functionCall.fFunction.fDefinition == nullptr) {
751+
if (functionDef == nullptr) {
752752
// Can't inline something if we don't actually have its definition.
753753
return false;
754754
}
755-
const FunctionDefinition& functionDef = *functionCall.fFunction.fDefinition;
756-
if (inlineThreshold < INT_MAX) {
757-
if (!(functionDef.fDeclaration.fModifiers.fFlags & Modifiers::kInline_Flag) &&
758-
Analysis::NodeCountExceeds(functionDef, inlineThreshold)) {
759-
// The function exceeds our maximum inline size and is not flagged 'inline'.
760-
return false;
761-
}
762-
}
755+
763756
if (!fSettings->fCaps || !fSettings->fCaps->canUseDoLoops()) {
764757
// We don't have do-while loops. We use do-while loops to simulate early returns, so we
765758
// can't inline functions that have an early return.
766-
bool hasEarlyReturn = has_early_return(functionDef);
759+
bool hasEarlyReturn = has_early_return(*functionDef);
767760

768761
// If we didn't detect an early return, there shouldn't be any returns in breakable
769762
// constructs either.
770-
SkASSERT(hasEarlyReturn || count_returns_in_breakable_constructs(functionDef) == 0);
763+
SkASSERT(hasEarlyReturn || count_returns_in_breakable_constructs(*functionDef) == 0);
771764
return !hasEarlyReturn;
772765
}
773766
// We have do-while loops, but we don't have any mechanism to simulate early returns within a
774767
// breakable construct (switch/for/do/while), so we can't inline if there's a return inside one.
775-
bool hasReturnInBreakableConstruct = (count_returns_in_breakable_constructs(functionDef) > 0);
768+
bool hasReturnInBreakableConstruct = (count_returns_in_breakable_constructs(*functionDef) > 0);
776769

777770
// If we detected returns in breakable constructs, we should also detect an early return.
778-
SkASSERT(!hasReturnInBreakableConstruct || has_early_return(functionDef));
771+
SkASSERT(!hasReturnInBreakableConstruct || has_early_return(*functionDef));
779772
return !hasReturnInBreakableConstruct;
780773
}
781774

782-
bool Inliner::analyze(Program& program) {
783-
// A candidate function for inlining, containing everything that `inlineCall` needs.
784-
struct InlineCandidate {
785-
SymbolTable* fSymbols; // the SymbolTable of the candidate
786-
std::unique_ptr<Statement>* fParentStmt; // the parent Statement of the enclosing stmt
787-
std::unique_ptr<Statement>* fEnclosingStmt; // the Statement containing the candidate
788-
std::unique_ptr<Expression>* fCandidateExpr; // the candidate FunctionCall to be inlined
789-
FunctionDefinition* fEnclosingFunction; // the Function containing the candidate
790-
};
791-
792-
// This is structured much like a ProgramVisitor, but does not actually use ProgramVisitor.
793-
// The analyzer needs to keep track of the `unique_ptr<T>*` of statements and expressions so
794-
// that they can later be replaced, and ProgramVisitor does not provide this; it only provides a
795-
// `const T&`.
796-
class InlineCandidateAnalyzer {
775+
// A candidate function for inlining, containing everything that `inlineCall` needs.
776+
struct InlineCandidate {
777+
SymbolTable* fSymbols; // the SymbolTable of the candidate
778+
std::unique_ptr<Statement>* fParentStmt; // the parent Statement of the enclosing stmt
779+
std::unique_ptr<Statement>* fEnclosingStmt; // the Statement containing the candidate
780+
std::unique_ptr<Expression>* fCandidateExpr; // the candidate FunctionCall to be inlined
781+
FunctionDefinition* fEnclosingFunction; // the Function containing the candidate
782+
bool fIsLargeFunction; // does candidate exceed the inline threshold?
783+
};
784+
785+
struct InlineCandidateList {
786+
std::vector<InlineCandidate> fCandidates;
787+
};
788+
789+
class InlineCandidateAnalyzer {
797790
public:
798791
// A list of all the inlining candidates we found during analysis.
799-
std::vector<InlineCandidate> fInlineCandidates;
792+
InlineCandidateList* fCandidateList;
793+
800794
// A stack of the symbol tables; since most nodes don't have one, expected to be shallower
801795
// than the enclosing-statement stack.
802796
std::vector<SymbolTable*> fSymbolTableStack;
@@ -807,14 +801,16 @@ bool Inliner::analyze(Program& program) {
807801
// The function that we're currently processing (i.e. inlining into).
808802
FunctionDefinition* fEnclosingFunction = nullptr;
809803

810-
void visit(Program& program) {
804+
void visit(Program& program, InlineCandidateList* candidateList) {
805+
fCandidateList = candidateList;
811806
fSymbolTableStack.push_back(program.fSymbols.get());
812807

813808
for (ProgramElement& pe : program) {
814809
this->visitProgramElement(&pe);
815810
}
816811

817812
fSymbolTableStack.pop_back();
813+
fCandidateList = nullptr;
818814
}
819815

820816
void visitProgramElement(ProgramElement* pe) {
@@ -1072,42 +1068,87 @@ bool Inliner::analyze(Program& program) {
10721068
}
10731069

10741070
void addInlineCandidate(std::unique_ptr<Expression>* candidate) {
1075-
fInlineCandidates.push_back(InlineCandidate{fSymbolTableStack.back(),
1076-
find_parent_statement(fEnclosingStmtStack),
1077-
fEnclosingStmtStack.back(),
1078-
candidate,
1079-
fEnclosingFunction});
1071+
fCandidateList->fCandidates.push_back(
1072+
InlineCandidate{fSymbolTableStack.back(),
1073+
find_parent_statement(fEnclosingStmtStack),
1074+
fEnclosingStmtStack.back(),
1075+
candidate,
1076+
fEnclosingFunction,
1077+
/*isLargeFunction=*/false});
10801078
}
1081-
};
1079+
};
10821080

1083-
InlineCandidateAnalyzer analyzer;
1084-
analyzer.visit(program);
1081+
bool Inliner::candidateCanBeInlined(const InlineCandidate& candidate, InlinabilityCache* cache) {
1082+
const FunctionDeclaration& funcDecl = (*candidate.fCandidateExpr)->as<FunctionCall>().fFunction;
10851083

1086-
// For each of our candidate function-call sites, check if it is actually safe to inline.
1087-
// Memoize our results so we don't check a function more than once.
1088-
std::unordered_map<const FunctionDeclaration*, bool> inlinableMap; // <function, safe-to-inline>
1089-
for (const InlineCandidate& candidate : analyzer.fInlineCandidates) {
1090-
const FunctionCall& funcCall = (*candidate.fCandidateExpr)->as<FunctionCall>();
1091-
const FunctionDeclaration* funcDecl = &funcCall.fFunction;
1092-
if (inlinableMap.find(funcDecl) == inlinableMap.end()) {
1093-
// We do not perform inlining on recursive calls to avoid an infinite death spiral of
1094-
// inlining.
1095-
int inlineThreshold = (funcDecl->fCallCount.load() > 1) ? fSettings->fInlineThreshold
1096-
: INT_MAX;
1097-
inlinableMap[funcDecl] = this->isSafeToInline(funcCall, inlineThreshold) &&
1098-
!contains_recursive_call(*funcDecl);
1099-
}
1084+
auto [iter, wasInserted] = cache->insert({&funcDecl, false});
1085+
if (wasInserted) {
1086+
// Recursion is forbidden here to avoid an infinite death spiral of inlining.
1087+
iter->second = this->isSafeToInline(funcDecl.fDefinition) &&
1088+
!contains_recursive_call(funcDecl);
1089+
}
1090+
1091+
return iter->second;
1092+
}
1093+
1094+
bool Inliner::isLargeFunction(const FunctionDefinition* functionDef) {
1095+
return Analysis::NodeCountExceeds(*functionDef, fSettings->fInlineThreshold);
1096+
}
1097+
1098+
bool Inliner::isLargeFunction(const InlineCandidate& candidate, LargeFunctionCache* cache) {
1099+
const FunctionDeclaration& funcDecl = (*candidate.fCandidateExpr)->as<FunctionCall>().fFunction;
1100+
1101+
auto [iter, wasInserted] = cache->insert({&funcDecl, false});
1102+
if (wasInserted) {
1103+
iter->second = this->isLargeFunction(funcDecl.fDefinition);
11001104
}
11011105

1106+
return iter->second;
1107+
}
1108+
1109+
void Inliner::buildCandidateList(Program& program, InlineCandidateList* candidateList) {
1110+
// This is structured much like a ProgramVisitor, but does not actually use ProgramVisitor.
1111+
// The analyzer needs to keep track of the `unique_ptr<T>*` of statements and expressions so
1112+
// that they can later be replaced, and ProgramVisitor does not provide this; it only provides a
1113+
// `const T&`.
1114+
InlineCandidateAnalyzer analyzer;
1115+
analyzer.visit(program, candidateList);
1116+
1117+
// Remove candidates that are not safe to inline.
1118+
std::vector<InlineCandidate>& candidates = candidateList->fCandidates;
1119+
InlinabilityCache cache;
1120+
candidates.erase(std::remove_if(candidates.begin(),
1121+
candidates.end(),
1122+
[&](const InlineCandidate& candidate) {
1123+
return !this->candidateCanBeInlined(candidate, &cache);
1124+
}),
1125+
candidates.end());
1126+
1127+
// Determine whether each candidate function exceeds our inlining size threshold or not. These
1128+
// can still be valid candidates if they are only called one time, so we don't remove them from
1129+
// the candidate list, but they will not be inlined if they're called more than once.
1130+
LargeFunctionCache largeFunctionCache;
1131+
for (InlineCandidate& candidate : candidates) {
1132+
candidate.fIsLargeFunction = this->isLargeFunction(candidate, &largeFunctionCache);
1133+
}
1134+
}
1135+
1136+
bool Inliner::analyze(Program& program) {
1137+
InlineCandidateList candidateList;
1138+
this->buildCandidateList(program, &candidateList);
1139+
11021140
// Inline the candidates where we've determined that it's safe to do so.
11031141
std::unordered_set<const std::unique_ptr<Statement>*> enclosingStmtSet;
11041142
bool madeChanges = false;
1105-
for (const InlineCandidate& candidate : analyzer.fInlineCandidates) {
1143+
for (const InlineCandidate& candidate : candidateList.fCandidates) {
11061144
FunctionCall& funcCall = (*candidate.fCandidateExpr)->as<FunctionCall>();
11071145
const FunctionDeclaration* funcDecl = &funcCall.fFunction;
11081146

1109-
// If we determined that this candidate was not actually inlinable, skip it.
1110-
if (!inlinableMap[funcDecl]) {
1147+
// If the function is large, not marked `inline`, and is called more than once, it's a bad
1148+
// idea to inline it.
1149+
if (candidate.fIsLargeFunction &&
1150+
!(funcDecl->fModifiers.fFlags & Modifiers::kInline_Flag) &&
1151+
funcDecl->fCallCount.load() > 1) {
11111152
continue;
11121153
}
11131154

src/sksl/SkSLInliner.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class Block;
2020
class Context;
2121
struct Expression;
2222
struct FunctionCall;
23+
struct FunctionDefinition;
24+
struct InlineCandidate;
25+
struct InlineCandidateList;
2326
struct Statement;
2427
class SymbolTable;
2528
struct Variable;
@@ -50,8 +53,11 @@ class Inliner {
5053
/** Adds a scope to inlined bodies returned by `inlineCall`, if one is required. */
5154
void ensureScopedBlocks(Statement* inlinedBody, Statement* parentStmt);
5255

53-
/** Checks whether inlining is viable for a FunctionCall. */
54-
bool isSafeToInline(const FunctionCall&, int inlineThreshold);
56+
/** Checks whether inlining is viable for a FunctionCall, modulo recursion and function size. */
57+
bool isSafeToInline(const FunctionDefinition* functionDef);
58+
59+
/** Checks whether a function's size exceeds the inline threshold from Settings. */
60+
bool isLargeFunction(const FunctionDefinition* functionDef);
5561

5662
/** Inlines any eligible functions that are found. Returns true if any changes are made. */
5763
bool analyze(Program& program);
@@ -61,6 +67,8 @@ class Inliner {
6167

6268
String uniqueNameForInlineVar(const String& baseName, SymbolTable* symbolTable);
6369

70+
void buildCandidateList(Program& program, InlineCandidateList* candidateList);
71+
6472
std::unique_ptr<Expression> inlineExpression(int offset,
6573
VariableRewriteMap* varMap,
6674
const Expression& expression);
@@ -72,6 +80,12 @@ class Inliner {
7280
const Statement& statement,
7381
bool isBuiltinCode);
7482

83+
using InlinabilityCache = std::unordered_map<const FunctionDeclaration*, bool>;
84+
bool candidateCanBeInlined(const InlineCandidate& candidate, InlinabilityCache* cache);
85+
86+
using LargeFunctionCache = std::unordered_map<const FunctionDeclaration*, bool>;
87+
bool isLargeFunction(const InlineCandidate& candidate, LargeFunctionCache* cache);
88+
7589
const Context* fContext = nullptr;
7690
const Program::Settings* fSettings = nullptr;
7791
int fInlineVarCounter = 0;

tests/sksl/inliner/golden/InlineKeywordOverridesThreshold.glsl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ void main() {
3838
++y;
3939
}
4040

41-
4241
{
4342
++y;
4443
++y;
@@ -76,5 +75,4 @@ void main() {
7675
++y;
7776
}
7877

79-
8078
}

tests/sksl/inliner/golden/InlineWithNestedBigCalls.glsl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ void main() {
4040
--_1_x;
4141
_1_x = 456.0;
4242
}
43-
4443
float _3_x = 456.0;
4544
{
4645
++_3_x;
@@ -79,7 +78,7 @@ void main() {
7978
--_3_x;
8079
_3_x = 123.0;
8180
}
82-
8381
sk_FragColor = vec4(123.0);
8482

83+
8584
}

0 commit comments

Comments
 (0)