Skip to content

Commit 6e7a4a0

Browse files
authored
Update TypeSSA for exact types (#7579)
TypeSSA replaces allocations with allocations of new subtypes of the original allocated type. This is not valid when the exactness of the original allocation can be observed, e.g. because the allocation can flow into some other instruction that would not be valid unless the allocation is exactly the original type. Fix TypeSSA for the presence of exact types by using ChildTyper to find instructions that require a child to be an exact reference. When such a child is found, inhibit all optimization of allocations for that child's type. This is the best we can do without performing a data flow analysis to prove that individual allocations do not flow into locations where their exactness can be observed.
1 parent 841b59f commit 6e7a4a0

File tree

5 files changed

+2055
-20
lines changed

5 files changed

+2055
-20
lines changed

scripts/test/fuzzing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
'optimize-instructions-struct-rmw.wast',
8585
'gto-removals-rmw.wast',
8686
'type-refining-rmw.wast',
87+
'type-ssa-exact-rmw.wast',
8788
'cfp-rmw.wast',
8889
# contains too many segments to run in a wasm VM
8990
'limit-segments_disable-bulk-memory.wast',
@@ -127,6 +128,7 @@
127128
'type-merging-exact.wast',
128129
'type-refining-exact.wast',
129130
'type-refining-gufa-exact.wast',
131+
'type-ssa-exact.wast',
130132
'minimize-rec-groups-exact.wast',
131133
'minimize-rec-groups-ignore-exact.wast',
132134
'public-exact.wast',

src/ir/child-typer.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ namespace wasm {
3333
// noteAnyType(Expression** childp) - The child may have any non-tuple type.
3434
// Used for the children of polymorphic instructions like `drop` and `select`.
3535
//
36-
// noteAnyReference(Expression** childp) - The child may have any reference
37-
// type. Used for the children of polymorphic reference instructions like
38-
// `ref.is_null`.
36+
// noteAnyReferenceType(Expression** childp) - The child may have any
37+
// reference type. Used for the children of polymorphic reference instructions
38+
// like `ref.is_null`.
3939
//
4040
// noteAnyTupleType(Expression** childp, size_t arity) - The child may have
4141
// any tuple type with the given arity. Used for the children of polymorphic

src/passes/TypeSSA.cpp

Lines changed: 172 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
// This pass works well with TypeMerging. See notes there for more.
4848
//
4949

50+
#include "ir/child-typer.h"
5051
#include "ir/find_all.h"
5152
#include "ir/module-utils.h"
5253
#include "ir/names.h"
@@ -147,14 +148,138 @@ std::vector<HeapType> ensureTypesAreInNewRecGroup(RecGroup recGroup,
147148
// A vector of struct.new or one of the variations on array.new.
148149
using News = std::vector<Expression*>;
149150

150-
struct NewFinder : public PostWalker<NewFinder> {
151+
// A set of types for which the exactness of allocations may be observed.
152+
using TypeSet = std::unordered_set<HeapType>;
153+
154+
struct Analyzer
155+
: public PostWalker<Analyzer, UnifiedExpressionVisitor<Analyzer>> {
156+
// Find allocations we can potentially optimize.
151157
News news;
152158

153-
void visitStructNew(StructNew* curr) { news.push_back(curr); }
154-
void visitArrayNew(ArrayNew* curr) { news.push_back(curr); }
155-
void visitArrayNewData(ArrayNewData* curr) { news.push_back(curr); }
156-
void visitArrayNewElem(ArrayNewElem* curr) { news.push_back(curr); }
157-
void visitArrayNewFixed(ArrayNewFixed* curr) { news.push_back(curr); }
159+
// Also find heap types for which the exactness of allocations is observed. We
160+
// will not be able to optimize allocations of these types without an analysis
161+
// proving that an allocation does not flow into any location where its
162+
// exactness is observed.
163+
TypeSet disallowedTypes;
164+
165+
// Find allocations we can potentially optimize.
166+
void visitStructNew(StructNew* curr) {
167+
news.push_back(curr);
168+
visitExpression(curr);
169+
}
170+
void visitArrayNew(ArrayNew* curr) {
171+
news.push_back(curr);
172+
visitExpression(curr);
173+
}
174+
void visitArrayNewData(ArrayNewData* curr) {
175+
news.push_back(curr);
176+
visitExpression(curr);
177+
}
178+
void visitArrayNewElem(ArrayNewElem* curr) {
179+
news.push_back(curr);
180+
visitExpression(curr);
181+
}
182+
void visitArrayNewFixed(ArrayNewFixed* curr) {
183+
news.push_back(curr);
184+
visitExpression(curr);
185+
}
186+
187+
// Find casts to exact types. Allocations of these types will not be able to
188+
// be optimized.
189+
template<typename Cast> void visitCast(Cast* cast) {
190+
if (auto type = cast->getCastType(); type.isExact()) {
191+
disallowedTypes.insert(type.getHeapType());
192+
}
193+
}
194+
void visitRefTest(RefTest* curr) { visitCast(curr); }
195+
void visitRefCast(RefCast* curr) { visitCast(curr); }
196+
void visitBrOn(BrOn* curr) {
197+
if (curr->op == BrOnCast || curr->op == BrOnCastFail) {
198+
visitCast(curr);
199+
}
200+
}
201+
202+
void visitExpression(Expression* curr) {
203+
// Look at the constraints on this expression's operands to see if it
204+
// requires an exact operand. If it does, we cannot optimize allocations of
205+
// that type. As an optimization, do not let control flow structures or
206+
// branches inhibit optimization since they can safely be refinalized to use
207+
// new types as long as no other instruction expected the original exact
208+
// type. Also allow optimizing if the instruction that would inhibit
209+
// optimizing will not be written in the final output. Skipping further
210+
// analysis for these instructions also ensures that the ChildTyper below
211+
// sees the type information it expects in the instructions it analyzes.
212+
if (Properties::isControlFlowStructure(curr) ||
213+
Properties::isBranch(curr) ||
214+
Properties::hasUnwritableTypeImmediate(curr)) {
215+
return;
216+
}
217+
218+
// Also do not let unreachable instructions inhibit optimization, as long as
219+
// they are unreachable because of an unreachable child. (Some other
220+
// unreachable instructions, such as a return_call, can still require an
221+
// exact operand and may inhibit optimization.)
222+
if (curr->type == Type::unreachable) {
223+
for (auto* child : ChildIterator(curr)) {
224+
if (child->type == Type::unreachable) {
225+
return;
226+
}
227+
}
228+
}
229+
230+
struct ExactChildTyper : ChildTyper<ExactChildTyper> {
231+
Analyzer& parent;
232+
ExactChildTyper(Analyzer& parent)
233+
: ChildTyper(*parent.getModule(), parent.getFunction()),
234+
parent(parent) {}
235+
236+
void noteSubtype(Expression**, Type type) {
237+
for (Type t : type) {
238+
if (t.isExact()) {
239+
parent.disallowedTypes.insert(t.getHeapType());
240+
}
241+
}
242+
}
243+
244+
// Other constraints do not matter to us.
245+
void noteAnyType(Expression**) {}
246+
void noteAnyReferenceType(Expression**) {}
247+
void noteAnyTupleType(Expression**, size_t) {}
248+
void noteAnyI8ArrayReferenceType(Expression**) {}
249+
void noteAnyI16ArrayReferenceType(Expression**) {}
250+
251+
Type getLabelType(Name label) { WASM_UNREACHABLE("unexpected branch"); }
252+
} typer(*this);
253+
typer.visit(curr);
254+
}
255+
256+
void visitFunction(Function* func) {
257+
// Returned exact references must remain exact references to the original
258+
// heap types.
259+
for (auto type : func->getSig().results) {
260+
if (type.isExact()) {
261+
disallowedTypes.insert(type.getHeapType());
262+
}
263+
}
264+
}
265+
266+
void visitGlobal(Global* global) {
267+
// This could be more precise by checking that the init expression is not
268+
// null before inhibiting optimization, or by just inhibiting optmization of
269+
// the allocations used in the initialization, but this is simpler.
270+
for (auto type : global->type) {
271+
if (type.isExact()) {
272+
disallowedTypes.insert(type.getHeapType());
273+
}
274+
}
275+
}
276+
277+
void visitElementSegment(ElementSegment* segment) {
278+
assert(!segment->type.isTuple());
279+
if (segment->type.isExact()) {
280+
disallowedTypes.insert(segment->type.getHeapType());
281+
}
282+
}
158283
};
159284

160285
struct TypeSSA : public Pass {
@@ -170,28 +295,48 @@ struct TypeSSA : public Pass {
170295
return;
171296
}
172297

173-
// First, find all the struct/array.news.
298+
struct Info {
299+
News news;
300+
TypeSet disallowedTypes;
301+
};
174302

175-
ModuleUtils::ParallelFunctionAnalysis<News> analysis(
176-
*module, [&](Function* func, News& news) {
303+
// First, analyze the function to find struct/array.news and disallowed
304+
// types.
305+
ModuleUtils::ParallelFunctionAnalysis<Info> analysis(
306+
*module, [&](Function* func, Info& info) {
177307
if (func->imported()) {
178308
return;
179309
}
180310

181-
NewFinder finder;
182-
finder.walk(func->body);
183-
news = std::move(finder.news);
311+
Analyzer analyzer;
312+
analyzer.walkFunctionInModule(func, module);
313+
info.news = std::move(analyzer.news);
314+
info.disallowedTypes = std::move(analyzer.disallowedTypes);
184315
});
185316

186317
// Also find news in the module scope.
187-
NewFinder moduleFinder;
188-
moduleFinder.walkModuleCode(module);
318+
Analyzer moduleAnalyzer;
319+
moduleAnalyzer.walkModuleCode(module);
320+
for (auto& global : module->globals) {
321+
moduleAnalyzer.visitGlobal(global.get());
322+
}
323+
for (auto& segment : module->elementSegments) {
324+
moduleAnalyzer.visitElementSegment(segment.get());
325+
}
326+
// TODO: Visit tables with initializers once we support those.
327+
328+
// Find all the types that are unoptimizable because the exactness of their
329+
// allocations may be observed.
330+
ModuleUtils::iterDefinedFunctions(*module, [&](Function* func) {
331+
processDisallowedTypes(analysis.map[func].disallowedTypes);
332+
});
333+
processDisallowedTypes(moduleAnalyzer.disallowedTypes);
189334

190335
// Process all the news to find the ones we want to modify, adding them to
191336
// newsToModify. Note that we must do so in a deterministic order.
192337
ModuleUtils::iterDefinedFunctions(
193-
*module, [&](Function* func) { processNews(analysis.map[func]); });
194-
processNews(moduleFinder.news);
338+
*module, [&](Function* func) { processNews(analysis.map[func].news); });
339+
processNews(moduleAnalyzer.news);
195340

196341
// Modify the ones we found are relevant. We must modify them all at once as
197342
// in the isorecursive type system we want to create a single new rec group
@@ -203,14 +348,24 @@ struct TypeSSA : public Pass {
203348
ReFinalize().runOnModuleCode(getPassRunner(), module);
204349
}
205350

351+
TypeSet disallowedTypes;
352+
353+
void processDisallowedTypes(const TypeSet& types) {
354+
disallowedTypes.insert(types.begin(), types.end());
355+
}
356+
206357
News newsToModify;
207358

208359
// As we generate new names, use a consistent index.
209360
Index nameCounter = 0;
210361

211362
void processNews(const News& news) {
212363
for (auto* curr : news) {
213-
if (isInteresting(curr)) {
364+
bool disallowed = false;
365+
if (curr->type.isRef()) {
366+
disallowed = disallowedTypes.count(curr->type.getHeapType());
367+
}
368+
if (!disallowed && isInteresting(curr)) {
214369
newsToModify.push_back(curr);
215370
}
216371
}

0 commit comments

Comments
 (0)