-
Notifications
You must be signed in to change notification settings - Fork 877
Expand file tree
/
Copy pathDeadArgumentElimination.cpp
More file actions
692 lines (627 loc) · 25.1 KB
/
Copy pathDeadArgumentElimination.cpp
File metadata and controls
692 lines (627 loc) · 25.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
/*
* Copyright 2018 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Optimizes call arguments in a whole-program manner. In particular, this
// removes ones that are not used (dead), but it also does more things:
//
// * Find functions for whom an argument is always passed the same
// constant. If so, we can just set that local to that constant
// in the function.
// * Find functions that don't use the value passed to an argument.
// If so, we can avoid even sending and receiving it. (Note how if
// the previous point was true for an argument, then the second
// must as well.)
// * Find return values ("return arguments" ;) that are never used.
// * Refine the types of arguments, that is make the argument type more
// specific if all the passed values allow that.
//
// This pass does not depend on flattening, but it may be more effective,
// as then call arguments never have side effects (which we need to
// watch for here).
//
#include <algorithm>
#include <unordered_map>
#include <unordered_set>
#include "ir/lubs.h"
#include "ir/return-utils.h"
#include "ir/type-updating.h"
#include "ir/utils.h"
#include "param-utils.h"
#include "pass.h"
#include "passes/opt-utils.h"
#include "support/sorted_vector.h"
#include "wasm-builder.h"
#include "wasm.h"
#ifndef DAE_DEBUG
#define DAE_DEBUG 0
#endif
#ifndef DAE_STATS
#define DAE_STATS 0
#endif
#if DAE_STATS
#include <iostream>
#endif // DAE_STATS
namespace wasm {
// Information for a function
struct DAEFunctionInfo {
// Whether this needs to be recomputed. This begins as true for the first
// computation, and we reset it every time we touch the function.
bool stale = true;
// The unused parameters, if any.
SortedVector unusedParams;
// Maps a function name to the calls going to it.
std::unordered_map<Name, std::vector<Call*>> calls;
// Map of all calls that are dropped, to their drops' locations (so that
// if we can optimize out the drop, we can replace the drop there).
std::unordered_map<Call*, Expression**> droppedCalls;
// Whether this function contains any tail calls (including indirect tail
// calls) and the set of functions this function tail calls. Tail-callers and
// tail-callees cannot have their dropped returns removed because of the
// constraint that tail-callees must have the same return type as
// tail-callers. Indirectly tail called functions are already not optimized
// because being in a table inhibits DAE. TODO: Allow the removal of dropped
// returns from tail-callers if their tail-callees can have their returns
// removed as well.
bool hasTailCalls = false;
std::unordered_set<Name> tailCallees;
// The set of functions that have calls from places that limit what we can do.
// For now, any call we don't see inhibits our optimizations, but TODO: an
// export could be worked around by exporting a thunk that adds the parameter.
//
// This is built up in parallel in each function, and combined at the end.
std::unordered_set<Name> hasUnseenCalls;
// Clears all data, which marks us as stale and in need of recomputation.
void clear() { *this = DAEFunctionInfo(); }
void markStale() { stale = true; }
};
using DAEFunctionInfoMap = std::unordered_map<Name, DAEFunctionInfo>;
struct DAEScanner
: public WalkerPass<PostWalker<DAEScanner, Visitor<DAEScanner>>> {
bool isFunctionParallel() override { return true; }
bool modifiesBinaryenIR() override { return false; }
std::unique_ptr<Pass> create() override {
return std::make_unique<DAEScanner>(infoMap);
}
DAEScanner(DAEFunctionInfoMap* infoMap) : infoMap(infoMap) {}
// The map of all infos for all functions.
DAEFunctionInfoMap* infoMap;
// The info for the function this instance operates on. We stash this as an
// optimization.
DAEFunctionInfo* info = nullptr;
void visitCall(Call* curr) {
if (!getModule()->getFunction(curr->target)->imported()) {
info->calls[curr->target].push_back(curr);
}
if (curr->isReturn) {
info->hasTailCalls = true;
info->tailCallees.insert(curr->target);
}
}
void visitCallIndirect(CallIndirect* curr) {
if (curr->isReturn) {
info->hasTailCalls = true;
}
}
void visitCallRef(CallRef* curr) {
if (curr->isReturn) {
info->hasTailCalls = true;
}
}
void visitDrop(Drop* curr) {
if (auto* call = curr->value->dynCast<Call>()) {
info->droppedCalls[call] = getCurrentPointer();
}
}
void visitRefFunc(RefFunc* curr) {
// RefFunc may be visited from either a function, in which case |info| was
// set, or module-level code (in which case we use the null function name in
// the infoMap).
auto* currInfo = info ? info : &(*infoMap)[Name()];
// Treat a ref.func as an unseen call, preventing us from changing the
// function's type. If we did change it, it could be an observable
// difference from the outside, if the reference escapes, for example.
// TODO: look for actual escaping?
// TODO: create a thunk for external uses that allow internal optimizations
currInfo->hasUnseenCalls.insert(curr->func);
}
// main entry point
void doWalkFunction(Function* func) {
// Set the info for this function.
info = &((*infoMap)[func->name]);
if (!info->stale) {
// Nothing changed since last time.
return;
}
// Clear the data, mark us as no longer stale, and recompute everything.
info->clear();
info->stale = false;
auto numParams = func->getNumParams();
PostWalker<DAEScanner, Visitor<DAEScanner>>::doWalkFunction(func);
// If there are params, check if they are used.
// TODO: This work could be avoided if we cannot optimize for other reasons.
// That would require deferring this to later and checking that.
if (numParams > 0) {
auto usedParams = ParamUtils::getUsedParams(func, getModule());
for (Index i = 0; i < numParams; i++) {
if (!usedParams.contains(i)) {
info->unusedParams.insert(i);
}
}
}
}
};
struct DAE : public Pass {
// This pass changes locals and parameters.
// FIXME DWARF updating does not handle local changes yet.
bool invalidatesDWARF() override { return true; }
bool optimize = false;
Index numFunctions;
// Map of function names to indexes. This lets us use indexes below for speed.
std::unordered_map<Name, Index> indexes;
void run(Module* module) override {
#if DAE_STATS
Index startParams = 0, startResults = 0;
for (auto& func : module->functions) {
startParams += func->getNumParams();
startResults += func->getResults().size();
}
#endif // DAE_STATS
DAEFunctionInfoMap infoMap;
// Ensure all entries exist so the parallel threads don't modify the data
// structure.
for (auto& func : module->functions) {
infoMap.try_emplace(func->name);
}
// The null name represents module-level code (not in a function).
infoMap.try_emplace(Name());
numFunctions = module->functions.size();
for (Index i = 0; i < numFunctions; i++) {
indexes[module->functions[i]->name] = i;
}
// Iterate to convergence.
while (1) {
if (!iteration(module, infoMap)) {
break;
}
}
#if DAE_STATS
Index endParams = 0, endResults = 0;
for (auto& func : module->functions) {
endParams += func->getNumParams();
endResults += func->getResults().size();
}
std::cout << "Removed parameters: " << (startParams - endParams) << "\n";
std::cout << "Removed results: " << (startResults - endResults) << "\n";
#endif // DAE_STATS
}
// For each function, the set of callers. This is used to propagate changes,
// e.g. if we remove a return value from a function, the calls might benefit
// from optimization. It is ok if this is an over-approximation, that is, if
// we think there are more callers than there are, as it would just lead to
// unneeded extra scanning of calling functions (in the example just given, if
// a caller did not actually call, they would not benefit from the extra
// optimization, but no harm is done, and no optimization missed). Such over-
// approximation can happen in later optimization iterations: We may manage to
// remove a call from a function to another (say, after applying a constant
// param, we see the call is not reached). This is somewhat rare, and the cost
// of computing this map is significant, so we compute it once at the start
// and then use that possibly-over-approximating data.
std::vector<std::vector<Name>> callers;
// A count of how many iterations we saw unprofitable removals of parameters.
// An unprofitable removal is one where we only manage to remove from a single
// call, that is, from one call target and it has a single call going to it.
// Such calls are not very interesting, as when there is a single call like
// that then inlining will handle it anyhow, in most cases, and inlining
// does so far more efficiently in situations of call chains:
//
// a -> b -> c -> d
//
// Imagine we remove a param from d, and so we remove it from the call in c.
// If c received that as a parameter, and only ever used it to call d, then
// now we can remove a param from c, and from the call in b, and so forth -
// requiring a full iteration each time to find the small amount of progress.
// (Inlining, otoh, will inline b into a, then c into a, and d into a,
// efficiently.)
Index unprofitableRemovalIters = 0;
bool iteration(Module* module, DAEFunctionInfoMap& infoMap) {
allDroppedCalls.clear();
#if DAE_DEBUG
// Enable this path to mark all contents as stale at the start of each
// iteration, which can be used to check for staleness bugs (that is, bugs
// where something should have been marked stale, but wasn't). Note, though,
// that staleness bugs can easily cause serious issues with validation (e.g.
// if data is stale we may miss that there is an additional caller, that
// prevents refining argument types etc.), so this may not be terribly
// helpful.
if (getenv("ALWAYS_MARK_STALE")) {
for (auto& [_, info] : infoMap) {
info.markStale();
}
}
#endif
DAEScanner scanner(&infoMap);
scanner.walkModuleCode(module);
// Scan all the functions.
scanner.run(getPassRunner(), module);
// Combine all the info from the scan.
std::vector<std::vector<Call*>> allCalls(numFunctions);
std::vector<bool> tailCallees(numFunctions);
std::vector<bool> hasUnseenCalls(numFunctions);
for (auto& [func, info] : infoMap) {
for (auto& [name, calls] : info.calls) {
auto& allCallsToName = allCalls[indexes[name]];
allCallsToName.insert(allCallsToName.end(), calls.begin(), calls.end());
}
for (auto& callee : info.tailCallees) {
tailCallees[indexes[callee]] = true;
}
for (auto& [call, dropp] : info.droppedCalls) {
allDroppedCalls[call] = dropp;
}
for (auto& name : info.hasUnseenCalls) {
hasUnseenCalls[indexes[name]] = true;
}
}
// Exports are considered unseen calls.
for (auto& curr : module->exports) {
if (curr->kind == ExternalKind::Function) {
hasUnseenCalls[indexes[*curr->getInternalName()]] = true;
}
}
// See comment above, we compute callers once and never again.
if (callers.empty()) {
// Compute first as sets, to deduplicate.
std::vector<std::unordered_set<Name>> callersSets(numFunctions);
for (auto& [func, info] : infoMap) {
for (auto& [name, calls] : info.calls) {
callersSets[indexes[name]].insert(func);
}
}
// Copy into efficient vectors.
callers.resize(numFunctions);
for (Index i = 0; i < numFunctions; ++i) {
auto& set = callersSets[i];
callers[i] = std::vector<Name>(set.begin(), set.end());
}
}
// Track which functions we changed that are worth re-optimizing at the end.
std::unordered_set<Function*> worthOptimizing;
// If we refine return types then we will need to do more type updating
// at the end.
bool refinedReturnTypes = false;
// If we find that localizing call arguments can help (by moving their
// effects outside, so ParamUtils::removeParameters can handle them), then
// we do that at the end and perform another cycle. It is simpler to just do
// another cycle than to track the locations of calls, which is tricky as
// localization might move a call (if a call happens to be another call's
// param). In practice it is rare to find call arguments we want to remove,
// and even more rare to find effects get in the way, so this should not
// cause much overhead.
//
// This set tracks the functions for whom calls to it should be modified.
std::unordered_set<Name> callTargetsToLocalize;
// As we optimize, we mark things as stale.
auto markStale = [&](Name func) {
// We only ever mark functions stale (not the global scope, which we never
// modify). An attempt to modify the global scope, identified by a null
// function name, is a logic bug.
assert(func.is());
infoMap[func].markStale();
};
auto markCallersStale = [&](Index index) {
for (auto caller : callers[index]) {
markStale(caller);
}
};
// We now have a mapping of all call sites for each function, and can look
// for optimization opportunities.
for (Index index = 0; index < numFunctions; index++) {
auto* func = module->functions[index].get();
if (func->imported()) {
continue;
}
// We can only optimize if we see all the calls and can modify them.
if (hasUnseenCalls[index]) {
continue;
}
auto& calls = allCalls[index];
if (calls.empty()) {
// Nothing calls this, so it is not worth optimizing.
continue;
}
// Refine argument types before doing anything else. This does not
// affect whether an argument is used or not, it just refines the type
// where possible.
auto name = func->name;
if (refineArgumentTypes(func, calls, module, infoMap[name])) {
worthOptimizing.insert(func);
markStale(func->name);
}
// Refine return types as well.
if (refineReturnTypes(func, calls, module)) {
refinedReturnTypes = true;
markStale(name);
markCallersStale(index);
}
auto optimizedIndexes =
ParamUtils::applyConstantValues({func}, calls, {}, module);
for (auto i : optimizedIndexes) {
// Mark it as unused, which we know it now is (no point to re-scan just
// for that).
infoMap[name].unusedParams.insert(i);
}
if (!optimizedIndexes.empty()) {
markStale(func->name);
}
}
if (refinedReturnTypes) {
// Changing a call expression's return type can propagate out to its
// parents, and so we must refinalize.
// TODO: We could track in which functions we actually make changes.
ReFinalize().run(getPassRunner(), module);
}
// We now know which parameters are unused, and can potentially remove them.
// Only do so if we didn't run into unprofitable removals - if so, leave
// any further removals for other invocations of this pass. (This avoids us
// getting stuck in long unprofitable call chains as mentioned in the
// comment earlier; note that we do process one unprofitable iteration
// before giving up here, so we do make progress at least.)
if (!unprofitableRemovalIters) {
Index removals = 0;
Index singleCallerRemovals = 0;
for (Index index = 0; index < numFunctions; index++) {
auto* func = module->functions[index].get();
if (func->imported()) {
continue;
}
if (hasUnseenCalls[index]) {
continue;
}
auto numParams = func->getNumParams();
if (numParams == 0) {
continue;
}
auto& calls = allCalls[index];
if (calls.empty()) {
continue;
}
auto name = func->name;
auto [removedIndexes, outcome] =
ParamUtils::removeParameters({func},
infoMap[name].unusedParams,
calls,
{},
module,
getPassRunner());
if (!removedIndexes.empty()) {
// Success!
worthOptimizing.insert(func);
markStale(name);
markCallersStale(index);
if (calls.size() == 1) {
singleCallerRemovals++;
}
removals++;
}
if (outcome == ParamUtils::RemovalOutcome::Failure) {
callTargetsToLocalize.insert(name);
}
}
if (removals == 1 && singleCallerRemovals == 1 &&
callTargetsToLocalize.empty()) {
// We only removed parameters from one function, and it had a single
// caller, and we don't have other pending actions (call targets we
// need to localize), so this was unprofitable as mentioned earlier.
unprofitableRemovalIters++;
}
}
// We can also tell which calls have all their return values dropped. Note
// that we can't do this if we changed anything so far, as we may have
// modified allCalls (we can't modify a call site twice in one iteration,
// once to remove a param, once to drop the return value).
if (worthOptimizing.empty()) {
for (Index index = 0; index < numFunctions; index++) {
auto& func = module->functions[index];
if (func->imported()) {
continue;
}
if (func->getResults() == Type::none) {
continue;
}
if (hasUnseenCalls[index]) {
continue;
}
auto name = func->name;
if (infoMap[name].hasTailCalls) {
continue;
}
if (tailCallees[index]) {
continue;
}
auto& calls = allCalls[index];
if (calls.empty()) {
continue;
}
bool allDropped =
std::all_of(calls.begin(), calls.end(), [&](Call* call) {
return allDroppedCalls.contains(call);
});
if (!allDropped) {
continue;
}
if (removeReturnValue(func.get(), calls, module)) {
// We should optimize the callers.
for (auto caller : callers[index]) {
worthOptimizing.insert(module->getFunction(caller));
}
}
// TODO Removing a drop may also open optimization opportunities in the
// callers.
worthOptimizing.insert(func.get());
markStale(name);
markCallersStale(index);
}
}
if (!callTargetsToLocalize.empty()) {
ParamUtils::localizeCallsTo(
callTargetsToLocalize, *module, getPassRunner(), [&](Function* func) {
markStale(func->name);
});
}
if (optimize && !worthOptimizing.empty()) {
OptUtils::optimizeAfterInlining(worthOptimizing, module, getPassRunner());
}
return !worthOptimizing.empty() || refinedReturnTypes ||
!callTargetsToLocalize.empty();
}
private:
std::unordered_map<Call*, Expression**> allDroppedCalls;
// Returns `true` if the caller should be optimized.
bool
removeReturnValue(Function* func, std::vector<Call*>& calls, Module* module) {
// If the result type is uninhabitable, then the caller knows the call will
// never return. That useful information would be lost if we did nothing
// else when removing the return value, but we will insert an `unreachable`
// after the call in the caller to preserve the optimization effect. TODO:
// Do this for more complicated uninhabitable types such as non-nullable
// references to structs with non-nullable reference cycles.
bool wasReturnUninhabitable =
func->getResults().isNull() && func->getResults().isNonNullable();
func->setResults(Type::none);
// Remove the drops on the calls. Note that we must do this before updating
// returns in ReturnUpdater, as there may be recursive calls of this
// function to itself. So we first use the information in allDroppedCalls
// before the ReturnUpdater potentially invalidates that information as it
// modifies the function.
for (auto* call : calls) {
auto iter = allDroppedCalls.find(call);
assert(iter != allDroppedCalls.end());
Expression** location = iter->second;
if (wasReturnUninhabitable) {
Builder builder(*module);
*location = builder.makeSequence(call, builder.makeUnreachable());
} else {
*location = call;
}
// Update the call's type.
if (call->type != Type::unreachable) {
call->type = Type::none;
}
}
// Remove any return values.
ReturnUtils::removeReturns(func, *module);
// It's definitely worth optimizing the caller after inserting the
// unreachable.
return wasReturnUninhabitable;
}
// Given a function and all the calls to it, see if we can refine the type of
// its arguments. If we only pass in a subtype, we may as well refine the type
// to that.
//
// This assumes that the function has no calls aside from |calls|, that is, it
// is not exported or called from the table or by reference.
bool refineArgumentTypes(Function* func,
const std::vector<Call*>& calls,
Module* module,
const DAEFunctionInfo& info) {
if (!module->features.hasGC()) {
return false;
}
auto numParams = func->getNumParams();
std::vector<Type> newParamTypes;
newParamTypes.reserve(numParams);
std::vector<LUBFinder> lubs(numParams);
for (Index i = 0; i < numParams; i++) {
auto originalType = func->getLocalType(i);
// If the parameter type is not a reference, there is nothing to refine.
// And if it is unused, also do nothing, as we can leave it to the other
// parts of this pass to optimize it properly, which avoids having to
// think about corner cases involving refining the type of an unused
// param (in particular, unused params are turned into locals, which means
// we'd need to think about defaultability etc.).
if (!originalType.isRef() || info.unusedParams.has(i)) {
newParamTypes.push_back(originalType);
continue;
}
auto& lub = lubs[i];
for (auto* call : calls) {
auto* operand = call->operands[i];
lub.note(operand->type);
if (lub.getLUB() == originalType) {
// We failed to refine this parameter to anything more specific.
break;
}
}
// Nothing is sent here at all; leave such optimizations to DCE.
if (!lub.noted()) {
return false;
}
newParamTypes.push_back(lub.getLUB());
}
// Check if we are able to optimize here before we do the work to scan the
// function body.
auto newParams = Type(newParamTypes);
if (newParams == func->getParams()) {
return false;
}
// We can do this!
TypeUpdating::updateParamTypes(func, newParamTypes, *module);
// Update the function's type.
func->setParams(newParams);
return true;
}
// See if the types returned from a function allow us to define a more refined
// return type for it. If so, we can update it and all calls going to it.
//
// This assumes that the function has no calls aside from |calls|, that is, it
// is not exported or called from the table or by reference. Exports should be
// fine, as should indirect calls in principle, but VMs will need to support
// function subtyping in indirect calls. TODO: relax this when possible
//
// Returns whether we optimized.
//
// TODO: We may be missing a global optimum here, as e.g. if a function calls
// itself and returns that value, then we would not do any change here,
// as one of the return values is exactly what it already is. Similar
// unoptimality can happen with multiple functions, more local code in
// the middle, etc.
bool refineReturnTypes(Function* func,
const std::vector<Call*>& calls,
Module* module) {
auto lub = LUB::getResultsLUB(func, *module);
if (!lub.noted()) {
return false;
}
auto newType = lub.getLUB();
if (newType != func->getResults()) {
func->setResults(newType);
for (auto* call : calls) {
if (call->type != Type::unreachable) {
call->type = newType;
}
}
return true;
}
return false;
}
};
Pass* createDAEPass() { return new DAE(); }
Pass* createDAEOptimizingPass() {
auto* ret = new DAE();
ret->optimize = true;
return ret;
}
} // namespace wasm