Skip to content

Commit ce7f869

Browse files
Refactor graph traversal in GlobalEffects (#8593)
Refactor GlobalEffects to not compute the transitive call graph explicitly but instead aggregate effects as we go. This improves the runtime of the pass by 4.3% on calcworker (1.21792 s -> 1.16586 s averaged over 20 compilations). It also helps prepare the code for future changes to support effects for indirect calls. Another potential future improvement here is to use SCC, which would let us stop processing children early in cases where there are no effects to update. Currently we can't do this because we add trap effects to potentially-recursive call loops, so even if no effects were updated, we need to keep going to find potential cycles.
1 parent 031e163 commit ce7f869

File tree

1 file changed

+143
-142
lines changed

1 file changed

+143
-142
lines changed

src/passes/GlobalEffects.cpp

Lines changed: 143 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -27,163 +27,162 @@
2727

2828
namespace wasm {
2929

30-
struct GenerateGlobalEffects : public Pass {
31-
void run(Module* module) override {
32-
// First, we do a scan of each function to see what effects they have,
33-
// including which functions they call directly (so that we can compute
34-
// transitive effects later).
35-
36-
struct FuncInfo {
37-
// Effects in this function.
38-
std::optional<EffectAnalyzer> effects;
39-
40-
// Directly-called functions from this function.
41-
std::unordered_set<Name> calledFunctions;
42-
};
43-
44-
ModuleUtils::ParallelFunctionAnalysis<FuncInfo> analysis(
45-
*module, [&](Function* func, FuncInfo& funcInfo) {
46-
if (func->imported()) {
47-
// Imports can do anything, so we need to assume the worst anyhow,
48-
// which is the same as not specifying any effects for them in the
49-
// map (which we do by not setting funcInfo.effects).
50-
return;
51-
}
52-
53-
// Gather the effects.
54-
funcInfo.effects.emplace(getPassOptions(), *module, func);
55-
56-
if (funcInfo.effects->calls) {
57-
// There are calls in this function, which we will analyze in detail.
58-
// Clear the |calls| field first, and we'll handle calls of all sorts
59-
// below.
60-
funcInfo.effects->calls = false;
61-
62-
// Clear throws as well, as we are "forgetting" calls right now, and
63-
// want to forget their throwing effect as well. If we see something
64-
// else that throws, below, then we'll note that there.
65-
funcInfo.effects->throws_ = false;
66-
67-
struct CallScanner
68-
: public PostWalker<CallScanner,
69-
UnifiedExpressionVisitor<CallScanner>> {
70-
Module& wasm;
71-
PassOptions& options;
72-
FuncInfo& funcInfo;
73-
74-
CallScanner(Module& wasm, PassOptions& options, FuncInfo& funcInfo)
75-
: wasm(wasm), options(options), funcInfo(funcInfo) {}
76-
77-
void visitExpression(Expression* curr) {
78-
ShallowEffectAnalyzer effects(options, wasm, curr);
79-
if (auto* call = curr->dynCast<Call>()) {
80-
// Note the direct call.
81-
funcInfo.calledFunctions.insert(call->target);
82-
} else if (effects.calls) {
83-
// This is an indirect call of some sort, so we must assume the
84-
// worst. To do so, clear the effects, which indicates nothing
85-
// is known (so anything is possible).
86-
// TODO: We could group effects by function type etc.
87-
funcInfo.effects.reset();
88-
} else {
89-
// No call here, but update throwing if we see it. (Only do so,
90-
// however, if we have effects; if we cleared it - see before -
91-
// then we assume the worst anyhow, and have nothing to update.)
92-
if (effects.throws_ && funcInfo.effects) {
93-
funcInfo.effects->throws_ = true;
94-
}
30+
namespace {
31+
32+
constexpr auto UnknownEffects = std::nullopt;
33+
34+
struct FuncInfo {
35+
// Effects in this function. nullopt / UnknownEffects means that we don't know
36+
// what effects this function has, so we conservatively assume all effects.
37+
// Nullopt cases won't be copied to Function::effects.
38+
std::optional<EffectAnalyzer> effects;
39+
40+
// Directly-called functions from this function.
41+
std::unordered_set<Name> calledFunctions;
42+
};
43+
44+
std::map<Function*, FuncInfo> analyzeFuncs(Module& module,
45+
const PassOptions& passOptions) {
46+
ModuleUtils::ParallelFunctionAnalysis<FuncInfo> analysis(
47+
module, [&](Function* func, FuncInfo& funcInfo) {
48+
if (func->imported()) {
49+
// Imports can do anything, so we need to assume the worst anyhow,
50+
// which is the same as not specifying any effects for them in the
51+
// map (which we do by not setting funcInfo.effects).
52+
return;
53+
}
54+
55+
// Gather the effects.
56+
funcInfo.effects.emplace(passOptions, module, func);
57+
58+
if (funcInfo.effects->calls) {
59+
// There are calls in this function, which we will analyze in detail.
60+
// Clear the |calls| field first, and we'll handle calls of all sorts
61+
// below.
62+
funcInfo.effects->calls = false;
63+
64+
// Clear throws as well, as we are "forgetting" calls right now, and
65+
// want to forget their throwing effect as well. If we see something
66+
// else that throws, below, then we'll note that there.
67+
funcInfo.effects->throws_ = false;
68+
69+
struct CallScanner
70+
: public PostWalker<CallScanner,
71+
UnifiedExpressionVisitor<CallScanner>> {
72+
Module& wasm;
73+
const PassOptions& options;
74+
FuncInfo& funcInfo;
75+
76+
CallScanner(Module& wasm,
77+
const PassOptions& options,
78+
FuncInfo& funcInfo)
79+
: wasm(wasm), options(options), funcInfo(funcInfo) {}
80+
81+
void visitExpression(Expression* curr) {
82+
ShallowEffectAnalyzer effects(options, wasm, curr);
83+
if (auto* call = curr->dynCast<Call>()) {
84+
// Note the direct call.
85+
funcInfo.calledFunctions.insert(call->target);
86+
} else if (effects.calls) {
87+
// This is an indirect call of some sort, so we must assume the
88+
// worst. To do so, clear the effects, which indicates nothing
89+
// is known (so anything is possible).
90+
// TODO: We could group effects by function type etc.
91+
funcInfo.effects = UnknownEffects;
92+
} else {
93+
// No call here, but update throwing if we see it. (Only do so,
94+
// however, if we have effects; if we cleared it - see before -
95+
// then we assume the worst anyhow, and have nothing to update.)
96+
if (effects.throws_ && funcInfo.effects) {
97+
funcInfo.effects->throws_ = true;
9598
}
9699
}
97-
};
98-
CallScanner scanner(*module, getPassOptions(), funcInfo);
99-
scanner.walkFunction(func);
100-
}
101-
});
102-
103-
// Compute the transitive closure of effects. To do so, first construct for
104-
// each function a list of the functions that it is called by (so we need to
105-
// propagate its effects to them), and then we'll construct the closure of
106-
// that.
107-
//
108-
// callers[foo] = [func that calls foo, another func that calls foo, ..]
109-
//
110-
std::unordered_map<Name, std::unordered_set<Name>> callers;
111-
112-
// Our work queue contains info about a new call pair: a call from a caller
113-
// to a called function, that is information we then apply and propagate.
114-
using CallPair = std::pair<Name, Name>; // { caller, called }
115-
UniqueDeferredQueue<CallPair> work;
116-
for (auto& [func, info] : analysis.map) {
117-
for (auto& called : info.calledFunctions) {
118-
work.push({func->name, called});
100+
}
101+
};
102+
CallScanner scanner(module, passOptions, funcInfo);
103+
scanner.walkFunction(func);
119104
}
105+
});
106+
107+
return std::move(analysis.map);
108+
}
109+
110+
// Propagate effects from callees to callers transitively
111+
// e.g. if A -> B -> C (A calls B which calls C)
112+
// Then B inherits effects from C and A inherits effects from both B and C.
113+
void propagateEffects(
114+
const Module& module,
115+
const std::unordered_map<Name, std::unordered_set<Name>>& reverseCallGraph,
116+
std::map<Function*, FuncInfo>& funcInfos) {
117+
118+
UniqueNonrepeatingDeferredQueue<std::pair<Name, Name>> work;
119+
120+
for (const auto& [callee, callers] : reverseCallGraph) {
121+
for (const auto& caller : callers) {
122+
work.push(std::pair(callee, caller));
120123
}
124+
}
121125

122-
// Compute the transitive closure of the call graph, that is, fill out
123-
// |callers| so that it contains the list of all callers - even through a
124-
// chain - of each function.
125-
while (!work.empty()) {
126-
auto [caller, called] = work.pop();
127-
128-
// We must not already have an entry for this call (that would imply we
129-
// are doing wasted work).
130-
assert(!callers[called].contains(caller));
131-
132-
// Apply the new call information.
133-
callers[called].insert(caller);
134-
135-
// We just learned that |caller| calls |called|. It also calls
136-
// transitively, which we need to propagate to all places unaware of that
137-
// information yet.
138-
//
139-
// caller => called => called by called
140-
//
141-
auto& calledInfo = analysis.map[module->getFunction(called)];
142-
for (auto calledByCalled : calledInfo.calledFunctions) {
143-
if (!callers[calledByCalled].contains(caller)) {
144-
work.push({caller, calledByCalled});
145-
}
146-
}
126+
auto propagate = [&](Name callee, Name caller) {
127+
auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects;
128+
const auto& calleeEffects =
129+
funcInfos.at(module.getFunction(callee)).effects;
130+
if (!callerEffects) {
131+
return;
147132
}
148133

149-
// Now that we have transitively propagated all static calls, apply that
150-
// information. First, apply infinite recursion: if a function can call
151-
// itself then it might recurse infinitely, which we consider an effect (a
152-
// trap).
153-
for (auto& [func, info] : analysis.map) {
154-
if (callers[func->name].contains(func->name)) {
155-
if (info.effects) {
156-
info.effects->trap = true;
157-
}
134+
if (!calleeEffects) {
135+
callerEffects = UnknownEffects;
136+
return;
137+
}
138+
139+
callerEffects->mergeIn(*calleeEffects);
140+
};
141+
142+
while (!work.empty()) {
143+
auto [callee, caller] = work.pop();
144+
145+
if (callee == caller) {
146+
auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects;
147+
if (callerEffects) {
148+
callerEffects->trap = true;
158149
}
159150
}
160151

161-
// Next, apply function effects to their callers.
162-
for (auto& [func, info] : analysis.map) {
163-
auto& funcEffects = info.effects;
164-
165-
for (auto& caller : callers[func->name]) {
166-
auto& callerEffects = analysis.map[module->getFunction(caller)].effects;
167-
if (!callerEffects) {
168-
// Nothing is known for the caller, which is already the worst case.
169-
continue;
170-
}
171-
172-
if (!funcEffects) {
173-
// Nothing is known for the called function, which means nothing is
174-
// known for the caller either.
175-
callerEffects.reset();
176-
continue;
177-
}
178-
179-
// Add func's effects to the caller.
180-
callerEffects->mergeIn(*funcEffects);
152+
// Even if nothing changed, we still need to keep traversing the callers
153+
// to look for a potential cycle which adds a trap affect on the above
154+
// lines.
155+
propagate(callee, caller);
156+
157+
const auto& callerCallers = reverseCallGraph.find(caller);
158+
if (callerCallers == reverseCallGraph.end()) {
159+
continue;
160+
}
161+
162+
for (const Name& callerCaller : callerCallers->second) {
163+
work.push(std::pair(callee, callerCaller));
164+
}
165+
}
166+
}
167+
168+
struct GenerateGlobalEffects : public Pass {
169+
void run(Module* module) override {
170+
std::map<Function*, FuncInfo> funcInfos =
171+
analyzeFuncs(*module, getPassOptions());
172+
173+
// callee : caller
174+
std::unordered_map<Name, std::unordered_set<Name>> callers;
175+
for (const auto& [func, info] : funcInfos) {
176+
for (const auto& callee : info.calledFunctions) {
177+
callers[callee].insert(func->name);
181178
}
182179
}
183180

181+
propagateEffects(*module, callers, funcInfos);
182+
184183
// Generate the final data, starting from a blank slate where nothing is
185184
// known.
186-
for (auto& [func, info] : analysis.map) {
185+
for (auto& [func, info] : funcInfos) {
187186
func->effects.reset();
188187
if (!info.effects) {
189188
continue;
@@ -202,6 +201,8 @@ struct DiscardGlobalEffects : public Pass {
202201
}
203202
};
204203

204+
} // namespace
205+
205206
Pass* createGenerateGlobalEffectsPass() { return new GenerateGlobalEffects(); }
206207

207208
Pass* createDiscardGlobalEffectsPass() { return new DiscardGlobalEffects(); }

0 commit comments

Comments
 (0)