Skip to content

Commit dd473d4

Browse files
authored
Track indirect call types in RemoveUnusedModuleElements (#7728)
An indirect call to a type in a table now only forces functions of that type to be marked as used: functions of other types are left alone, potentially leaving them unreached. This is more precise than assuming any indirect call can reach anywhere, which is more or less what we did before. The downside is that this makes the pass is 10% slower. This is one of our faster passes, however, so the cost seems worth it, given that this can noticeably help in some cases, e.g., 6.5% code size reduction on Poppler, and 20% on an embind testcase. In general, however, most real-world codebases benefit little if at all, because there is too much overlap in indirect call signatures: Statistically, when you generate random graphs of size `n` and chance for each edge to exist `p`, then even if `p` decreases to 0 the graph will tend to end up fully connected [1]. And, in wasm, `p` does not even decrease to 0: * Consider some common signature like `{i32} -> {}` (i32 param, no result). * In real-world code, there is some chance `q>0` for that signature to be called, and some chance `r>0` for that signature to exist in the code. * `p >= O(rq) > 0` because all it takes for a connection to exist is that that signature exists on one side and is called on the other. That is, in large codebases there is an overlap in signatures, and statistically this means that all the code will end up reachable, at least in the limit. But some programs do get lucky and benefit from this PR. [1] https://en.wikipedia.org/wiki/Erd%C5%91s%E2%80%93R%C3%A9nyi_model#Properties_of_G(n,_p)
1 parent 452e3ee commit dd473d4

13 files changed

+703
-120
lines changed

src/passes/RemoveUnusedModuleElements.cpp

Lines changed: 116 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
// * No references at all. We can simply remove it.
2525
// * References, but no uses. We can't remove it, but we can change it (see
2626
// below).
27-
// * Uses (which imply references). We must keep it as it is.
27+
// * Uses (which imply references). We must keep it as it is, because it is
28+
// fully used (e.g. for a function, it is called and may execute).
2829
//
2930
// An example of something with a reference but *not* a use is a RefFunc to a
3031
// function that has no corresponding CallRef to that type. We cannot just
@@ -62,25 +63,38 @@ using ModuleElementKind = ModuleItemKind;
6263
// name of the particular element.
6364
using ModuleElement = std::pair<ModuleElementKind, Name>;
6465

66+
// Information from an indirect call: the name of the table, and the heap type.
67+
using IndirectCall = std::pair<Name, HeapType>;
68+
6569
// Visit or walk an expression to find what things are referenced.
6670
struct ReferenceFinder
6771
: public PostWalker<ReferenceFinder,
6872
UnifiedExpressionVisitor<ReferenceFinder>> {
6973
// Our findings are placed in these data structures, which the user of this
70-
// code can then process.
71-
std::vector<ModuleElement> elements;
74+
// code can then process. We mark both uses and references, and also note
75+
// uses of specific things that require special handling, like refFuncs.
76+
std::vector<ModuleElement> used, referenced;
7277
std::vector<HeapType> callRefTypes;
7378
std::vector<Name> refFuncs;
7479
std::vector<StructField> structFields;
80+
std::vector<IndirectCall> indirectCalls;
7581

7682
// Add an item to the output data structures.
77-
void note(ModuleElement element) { elements.push_back(element); }
78-
void noteCallRef(HeapType type) { callRefTypes.push_back(type); }
79-
void noteRefFunc(Name refFunc) { refFuncs.push_back(refFunc); }
80-
void note(StructField structField) { structFields.push_back(structField); }
81-
82-
// Generic visitor
83+
void use(ModuleElement element) { used.push_back(element); }
84+
void reference(ModuleElement element) { referenced.push_back(element); }
85+
void useCallRef(HeapType type) { callRefTypes.push_back(type); }
86+
void useRefFunc(Name refFunc) { refFuncs.push_back(refFunc); }
87+
void useStructField(StructField structField) {
88+
structFields.push_back(structField);
89+
}
90+
void useIndirectCall(Name table, HeapType type) {
91+
indirectCalls.push_back({table, type});
92+
}
8393

94+
// Generic visitor: Use all the things referenced. This handles e.g. using the
95+
// table of a table.get. When we do not want such unconditional use, we
96+
// override (e.g. for call_indirect, we don't want to mark the entire table as
97+
// used, see below).
8498
void visitExpression(Expression* curr) {
8599
#define DELEGATE_ID curr->_id
86100

@@ -101,7 +115,7 @@ struct ReferenceFinder
101115

102116
#define DELEGATE_FIELD_NAME_KIND(id, field, kind) \
103117
if (cast->field.is()) { \
104-
note({kind, cast->field}); \
118+
use({kind, cast->field}); \
105119
}
106120

107121
#include "wasm-delegations-fields.def"
@@ -110,7 +124,7 @@ struct ReferenceFinder
110124
// Specific visitors
111125

112126
void visitCall(Call* curr) {
113-
note({ModuleElementKind::Function, curr->target});
127+
use({ModuleElementKind::Function, curr->target});
114128

115129
if (Intrinsics(*getModule()).isCallWithoutEffects(curr)) {
116130
// A call-without-effects receives a function reference and calls it, the
@@ -137,12 +151,15 @@ struct ReferenceFinder
137151
}
138152

139153
void visitCallIndirect(CallIndirect* curr) {
140-
note({ModuleElementKind::Table, curr->table});
154+
// We refer to the table, but may not use all parts of it, that depends on
155+
// the heap type we call with.
156+
reference({ModuleElementKind::Table, curr->table});
157+
useIndirectCall(curr->table, curr->heapType);
141158
// Note a possible call of a function reference as well, as something might
142159
// be written into the table during runtime. With precise tracking of what
143160
// is written into the table we could do better here; we could also see
144161
// which tables are immutable. TODO
145-
noteCallRef(curr->heapType);
162+
useCallRef(curr->heapType);
146163
}
147164

148165
void visitCallRef(CallRef* curr) {
@@ -151,17 +168,17 @@ struct ReferenceFinder
151168
return;
152169
}
153170

154-
noteCallRef(curr->target->type.getHeapType());
171+
useCallRef(curr->target->type.getHeapType());
155172
}
156173

157-
void visitRefFunc(RefFunc* curr) { noteRefFunc(curr->func); }
174+
void visitRefFunc(RefFunc* curr) { useRefFunc(curr->func); }
158175

159176
void visitStructGet(StructGet* curr) {
160177
if (curr->ref->type == Type::unreachable || curr->ref->type.isNull()) {
161178
return;
162179
}
163180
auto type = curr->ref->type.getHeapType();
164-
note(StructField{type, curr->index});
181+
useStructField(StructField{type, curr->index});
165182
}
166183
};
167184

@@ -262,9 +279,12 @@ struct Analyzer {
262279
ReferenceFinder finder;
263280
finder.setModule(module);
264281
finder.visit(curr);
265-
for (auto element : finder.elements) {
282+
for (auto element : finder.used) {
266283
use(element);
267284
}
285+
for (auto element : finder.referenced) {
286+
reference(element);
287+
}
268288
for (auto type : finder.callRefTypes) {
269289
useCallRefType(type);
270290
}
@@ -274,6 +294,9 @@ struct Analyzer {
274294
for (auto structField : finder.structFields) {
275295
useStructField(structField);
276296
}
297+
for (auto call : finder.indirectCalls) {
298+
useIndirectCall(call);
299+
}
277300

278301
// Scan the children to continue our work.
279302
scanChildren(curr);
@@ -316,6 +339,37 @@ struct Analyzer {
316339
}
317340
}
318341

342+
std::unordered_set<IndirectCall> usedIndirectCalls;
343+
344+
void useIndirectCall(IndirectCall call) {
345+
auto [_, inserted] = usedIndirectCalls.insert(call);
346+
if (!inserted) {
347+
return;
348+
}
349+
350+
// TODO: use structured bindings with c++20, needed for the capture below
351+
auto table = call.first;
352+
auto type = call.second;
353+
354+
// Any function in the table of that signature may be called.
355+
ModuleUtils::iterTableSegments(
356+
*module, table, [&](ElementSegment* segment) {
357+
auto segmentReferenced = false;
358+
for (auto* item : segment->data) {
359+
if (auto* refFunc = item->dynCast<RefFunc>()) {
360+
auto* func = module->getFunction(refFunc->func);
361+
if (HeapType::isSubType(func->type, type)) {
362+
use({ModuleElementKind::Function, refFunc->func});
363+
segmentReferenced = true;
364+
}
365+
}
366+
}
367+
if (segmentReferenced) {
368+
reference({ModuleElementKind::ElementSegment, segment->name});
369+
}
370+
});
371+
}
372+
319373
void useRefFunc(Name func) {
320374
if (!options.closedWorld) {
321375
// The world is open, so assume the worst and something (inside or outside
@@ -341,7 +395,7 @@ struct Analyzer {
341395
// We've never seen a CallRef for this, but might see one later.
342396
uncalledRefFuncMap[type].insert(func);
343397

344-
referenced.insert(element);
398+
reference(element);
345399
}
346400
}
347401

@@ -554,34 +608,11 @@ struct Analyzer {
554608
finder.setModule(module);
555609
finder.walk(curr);
556610

557-
for (auto element : finder.elements) {
558-
// Avoid repeated work. Note that globals with multiple references to
559-
// previous globals can lead to exponential work, so this is important.
560-
// (If C refers twice to B, and B refers twice to A, then when we process
561-
// C we would, naively, scan B twice and A four times.)
562-
auto [_, inserted] = referenced.insert(element);
563-
if (!inserted) {
564-
continue;
565-
}
566-
567-
auto& [kind, value] = element;
568-
if (kind == ModuleElementKind::Global) {
569-
// Like functions, (non-imported) globals have contents. For functions,
570-
// things are simple: if a function ends up with references but no uses
571-
// then we can simply empty out the function (by setting its body to an
572-
// unreachable). We don't have a simple way to do the same for globals,
573-
// unfortunately. For now, scan the global's contents and add references
574-
// as needed.
575-
// TODO: We could try to empty the global out, for example, replace it
576-
// with a null if it is nullable, or replace all gets of it with
577-
// something else, but that is not trivial.
578-
auto* global = module->getGlobal(value);
579-
if (!global->imported()) {
580-
// Note that infinite recursion is not a danger here since a global
581-
// can only refer to previous globals.
582-
addReferences(global->init);
583-
}
584-
}
611+
for (auto element : finder.used) {
612+
reference(element);
613+
}
614+
for (auto element : finder.referenced) {
615+
reference(element);
585616
}
586617

587618
for (auto func : finder.refFuncs) {
@@ -594,7 +625,7 @@ struct Analyzer {
594625
// just adding a reference to the function, and not actually using the
595626
// RefFunc. (Only useRefFunc() + a CallRef of the proper type are enough
596627
// to make a function itself used.)
597-
referenced.insert({ModuleElementKind::Function, func});
628+
reference({ModuleElementKind::Function, func});
598629
}
599630

600631
// Note: nothing to do with |callRefTypes| and |structFields|, which only
@@ -603,6 +634,44 @@ struct Analyzer {
603634
// handled in an entirely different way in Binaryen IR, and we don't need to
604635
// worry about it.)
605636
}
637+
638+
void reference(ModuleElement element) {
639+
// Avoid repeated work. Note that globals with multiple references to
640+
// previous globals can lead to exponential work, so this is important.
641+
// (If C refers twice to B, and B refers twice to A, then when we process
642+
// C we would, naively, scan B twice and A four times.)
643+
auto [_, inserted] = referenced.insert(element);
644+
if (!inserted) {
645+
return;
646+
}
647+
648+
// Some references force references to their internals, just by being
649+
// referenced and present in the output.
650+
auto& [kind, value] = element;
651+
if (kind == ModuleElementKind::Global) {
652+
// Like functions, (non-imported) globals have contents. For functions,
653+
// things are simple: if a function ends up with references but no uses
654+
// then we can simply empty out the function (by setting its body to an
655+
// unreachable). We don't have a simple way to do the same for globals,
656+
// unfortunately. For now, scan the global's contents and add references
657+
// as needed.
658+
// TODO: We could try to empty the global out, for example, replace it
659+
// with a null if it is nullable, or replace all gets of it with
660+
// something else, but that is not trivial.
661+
auto* global = module->getGlobal(value);
662+
if (!global->imported()) {
663+
// Note that infinite recursion is not a danger here since a global
664+
// can only refer to previous globals.
665+
addReferences(global->init);
666+
}
667+
} else if (kind == ModuleElementKind::ElementSegment) {
668+
// TODO: We could empty out parts of the segment we don't need.
669+
auto* segment = module->getElementSegment(value);
670+
for (auto* item : segment->data) {
671+
addReferences(item);
672+
}
673+
}
674+
}
606675
};
607676

608677
struct RemoveUnusedModuleElements : public Pass {
@@ -709,13 +778,6 @@ struct RemoveUnusedModuleElements : public Pass {
709778
}
710779
});
711780

712-
// For now, all functions that can be called indirectly are marked as roots.
713-
// TODO: Compute this based on which ElementSegments are actually used,
714-
// and which functions have a call_indirect of the proper type.
715-
ElementUtils::iterAllElementFunctionNames(module, [&](Name name) {
716-
roots.emplace_back(ModuleElementKind::Function, name);
717-
});
718-
719781
// Just as out-of-bound segments may cause observable traps at instantiation
720782
// time, so can struct.new instructions with null descriptors cause traps in
721783
// global or element segment initializers.

test/ctor-eval/bad-indirect-call3.wast.out

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
(module
2-
(type $0 (func (param externref)))
3-
(type $1 (func))
2+
(type $0 (func))
43
(type $funcref_=>_none (func (param funcref)))
5-
(memory $0 256 256)
6-
(data $0 (i32.const 10) "waka waka waka waka waka")
74
(table $0 1 1 funcref)
8-
(elem $implicit-elem (i32.const 0) $callee)
95
(export "sig_mismatch" (func $sig_mismatch))
10-
(func $callee (type $0) (param $0 externref)
11-
(i32.store8
12-
(i32.const 40)
13-
(i32.const 67)
14-
)
15-
)
16-
(func $sig_mismatch (type $1)
6+
(func $sig_mismatch (type $0)
177
(call_indirect $0 (type $funcref_=>_none)
188
(ref.null nofunc)
199
(i32.const 0)

test/ctor-eval/basics-flatten.wast

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(module
22
(type $v (func))
3-
(memory 256 256)
3+
(memory $m 256 256)
44
;; test flattening of multiple segments
55
(data (i32.const 10) "waka ")
66
(data (i32.const 15) "waka") ;; skip a byte here
@@ -10,6 +10,7 @@
1010
(export "test1" (func $test1))
1111
(export "test2" (func $test2))
1212
(export "test3" (func $test3))
13+
(export "memory" (memory $m)) ;; export memory so we can see the flattened data
1314
(func $test1
1415
(drop (i32.const 0)) ;; no work at all, really
1516
(call $safe-to-call) ;; safe to call
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
(module
2-
(type $v (func))
3-
(memory $0 256 256)
2+
(memory $m 256 256)
43
(data $0 (i32.const 10) "nas\00\00\00aka\00yzkx waka wakm\00\00\00\00\00\00C")
5-
(func $call-indirect (type $v)
6-
(i32.store8
7-
(i32.const 40)
8-
(i32.const 67)
9-
)
10-
)
4+
(export "memory" (memory $m))
115
)

test/ctor-eval/basics.wast

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
(module
22
(type $v (func))
3-
(memory 256 256)
3+
(memory $m 256 256)
44
(data (i32.const 10) "waka waka waka waka waka")
55
(table 1 1 funcref)
66
(elem (i32.const 0) $call-indirect)
77
(export "test1" (func $test1))
88
(export "test2" (func $test2))
99
(export "test3" (func $test3))
10+
(export "memory" (memory $m)) ;; export memory so we can see the updated data
1011
(func $test1
1112
(drop (i32.const 0)) ;; no work at all, really
1213
(call $safe-to-call) ;; safe to call

test/ctor-eval/basics.wast.out

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
(module
2-
(type $v (func))
3-
(memory $0 256 256)
2+
(memory $m 256 256)
43
(data $0 (i32.const 10) "nas\00\00\00aka yzkx waka wakm\00\00\00\00\00\00C")
5-
(func $call-indirect (type $v)
6-
(i32.store8
7-
(i32.const 40)
8-
(i32.const 67)
9-
)
10-
)
4+
(export "memory" (memory $m))
115
)

test/ctor-eval/indirect-call3.wast

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
(module
22
(import "env" "_abort" (func $_abort))
33
(type $v (func))
4-
(memory 256 256)
4+
(memory $m 256 256)
55
(data (i32.const 10) "waka waka waka waka waka")
66
(table 2 2 funcref)
77
(elem (i32.const 0) $_abort $call-indirect)
88
(export "test1" (func $test1))
9+
(export "memory" (memory $m)) ;; export memory so we can see the updated data
910
(func $test1
1011
(call_indirect (type $v) (i32.const 1)) ;; safe to call
1112
(i32.store8 (i32.const 20) (i32.const 120))

0 commit comments

Comments
 (0)