Skip to content

Commit a4f9128

Browse files
authored
GlobalTypeOptimization: Reorder fields in order to remove them (#6820)
Before, we only removed fields from the end of a struct. If we had, say struct Foo { int x; int y; int z; }; // Add no fields but inherit the parent's. struct Bar : Foo {}; If y is only used in Bar, but never Foo, then we still kept it around, because if we removed it from Foo we'd end up with Foo = {x, z}, Bar = {x, y, z} which is invalid - Bar no longer extends Foo. But we can do this if we first reorder the two: struct Foo { int x; int z; int y; // now y is at the end }; struct Bar : Foo {}; And the optimized form is struct Foo { int x; int z; }; struct Bar : Foo { int y; // now y is added in Bar }; This lets us remove all fields possible in all cases AFAIK. This situation is not super-common, as most fields are actually used both up and down the hierarchy (if they are used at all), but testing on some large real-world codebases, I see 10 fields removed in Java, 45 in Kotlin, and 31 in Dart testcases. The NFC change to src/wasm-type-ordering.h was needed for this to compile.
1 parent e729e01 commit a4f9128

File tree

4 files changed

+594
-70
lines changed

4 files changed

+594
-70
lines changed

src/ir/localize.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ struct ChildLocalizer {
130130
// effects we can't remove, or if it interacts with other children.
131131
bool needLocal = effects[i].hasUnremovableSideEffects();
132132
if (!needLocal) {
133+
// TODO: Avoid quadratic time here by accumulating effects and checking
134+
// vs the accumulation.
133135
for (Index j = 0; j < num; j++) {
134136
if (j != i && effects[i].invalidates(effects[j])) {
135137
needLocal = true;

src/passes/GlobalTypeOptimization.cpp

Lines changed: 149 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
#include "ir/type-updating.h"
3030
#include "ir/utils.h"
3131
#include "pass.h"
32+
#include "support/permutations.h"
3233
#include "wasm-builder.h"
34+
#include "wasm-type-ordering.h"
3335
#include "wasm-type.h"
3436
#include "wasm.h"
3537

@@ -160,19 +162,23 @@ struct GlobalTypeOptimization : public Pass {
160162
// immutable). Note that by making more things immutable we therefore
161163
// make it possible to apply more specific subtypes in subtype fields.
162164
StructUtils::TypeHierarchyPropagator<FieldInfo> propagator(*module);
163-
auto subSupers = combinedSetGetInfos;
164-
propagator.propagateToSuperAndSubTypes(subSupers);
165-
auto subs = std::move(combinedSetGetInfos);
166-
propagator.propagateToSubTypes(subs);
167-
168-
// Process the propagated info.
169-
for (auto type : propagator.subTypes.types) {
165+
auto dataFromSubsAndSupersMap = combinedSetGetInfos;
166+
propagator.propagateToSuperAndSubTypes(dataFromSubsAndSupersMap);
167+
auto dataFromSupersMap = std::move(combinedSetGetInfos);
168+
propagator.propagateToSubTypes(dataFromSupersMap);
169+
170+
// Process the propagated info. We look at supertypes first, as the order of
171+
// fields in a supertype is a constraint on what subtypes can do. That is,
172+
// we decide for each supertype what the optimal order is, and consider that
173+
// fixed, and then subtypes can decide how to sort fields that they append.
174+
HeapTypeOrdering::SupertypesFirst sorted;
175+
for (auto type : sorted.sort(propagator.subTypes.types)) {
170176
if (!type.isStruct()) {
171177
continue;
172178
}
173179
auto& fields = type.getStruct().fields;
174-
auto& subSuper = subSupers[type];
175-
auto& sub = subs[type];
180+
auto& dataFromSubsAndSupers = dataFromSubsAndSupersMap[type];
181+
auto& dataFromSupers = dataFromSupersMap[type];
176182

177183
// Process immutability.
178184
for (Index i = 0; i < fields.size(); i++) {
@@ -181,7 +187,7 @@ struct GlobalTypeOptimization : public Pass {
181187
continue;
182188
}
183189

184-
if (subSuper[i].hasWrite) {
190+
if (dataFromSubsAndSupers[i].hasWrite) {
185191
// A set exists.
186192
continue;
187193
}
@@ -192,48 +198,132 @@ struct GlobalTypeOptimization : public Pass {
192198
vec[i] = true;
193199
}
194200

195-
// Process removability. We check separately for the ability to
196-
// remove in a general way based on sub+super-propagated info (that is,
197-
// fields that are not used in sub- or super-types, and so we can
198-
// definitely remove them from all the relevant types) and also in the
199-
// specific way that only works for removing at the end, which as
200-
// mentioned above only looks at super-types.
201+
// Process removability.
201202
std::set<Index> removableIndexes;
202203
for (Index i = 0; i < fields.size(); i++) {
203-
if (!subSuper[i].hasRead) {
204-
removableIndexes.insert(i);
205-
}
206-
}
207-
for (int i = int(fields.size()) - 1; i >= 0; i--) {
208-
// Unlike above, a write would stop us here: above we propagated to both
209-
// sub- and super-types, which means if we see no reads then there is no
210-
// possible read of the data at all. But here we just propagated to
211-
// subtypes, and so we need to care about the case where the parent
212-
// writes to a field but does not read from it - we still need those
213-
// writes to happen as children may read them. (Note that if no child
214-
// reads this field, and since we check for reads in parents here, that
215-
// means the field is not read anywhere at all, and we would have
216-
// handled that case in the previous loop anyhow.)
217-
if (!sub[i].hasRead && !sub[i].hasWrite) {
204+
// If there is no read whatsoever, in either subs or supers, then we can
205+
// remove the field. That is so even if there are writes (it would be a
206+
// pointless "write-only field").
207+
auto hasNoReadsAnywhere = !dataFromSubsAndSupers[i].hasRead;
208+
209+
// Check for reads or writes in ourselves and our supers. If there are
210+
// none, then operations only happen in our strict subtypes, and those
211+
// subtypes can define the field there, and we don't need it here.
212+
auto hasNoReadsOrWritesInSupers =
213+
!dataFromSupers[i].hasRead && !dataFromSupers[i].hasWrite;
214+
215+
if (hasNoReadsAnywhere || hasNoReadsOrWritesInSupers) {
218216
removableIndexes.insert(i);
219-
} else {
220-
// Once we see something we can't remove, we must stop, as we can only
221-
// remove from the end in this case.
222-
break;
223217
}
224218
}
225-
if (!removableIndexes.empty()) {
226-
auto& indexesAfterRemoval = indexesAfterRemovals[type];
227-
indexesAfterRemoval.resize(fields.size());
228-
Index skip = 0;
229-
for (Index i = 0; i < fields.size(); i++) {
230-
if (!removableIndexes.count(i)) {
231-
indexesAfterRemoval[i] = i - skip;
219+
220+
// We need to compute the new set of indexes if we are removing fields, or
221+
// if our parent removed fields. In the latter case, our parent may have
222+
// reordered fields even if we ourselves are not removing anything, and we
223+
// must update to match the parent's order.
224+
auto super = type.getDeclaredSuperType();
225+
auto superHasUpdates = super && indexesAfterRemovals.count(*super);
226+
if (!removableIndexes.empty() || superHasUpdates) {
227+
// We are removing fields. Reorder them to allow that, as in the general
228+
// case we can only remove fields from the end, so that if our subtypes
229+
// still need the fields they can append them. For example:
230+
//
231+
// type A = { x: i32, y: f64 };
232+
// type B : A = { x: 132, y: f64, z: v128 };
233+
//
234+
// If field x is used in B but never in A then we want to remove it, but
235+
// we cannot end up with this:
236+
//
237+
// type A = { y: f64 };
238+
// type B : A = { x: 132, y: f64, z: v128 };
239+
//
240+
// Here B no longer extends A's fields. Instead, we reorder A, which
241+
// then imposes the same order on B's fields:
242+
//
243+
// type A = { y: f64, x: i32 };
244+
// type B : A = { y: f64, x: i32, z: v128 };
245+
//
246+
// And after that, it is safe to remove x in A: B will then append it,
247+
// just like it appends z, leading to this:
248+
//
249+
// type A = { y: f64 };
250+
// type B : A = { y: f64, x: i32, z: v128 };
251+
//
252+
std::vector<Index> indexesAfterRemoval(fields.size());
253+
254+
// The next new index to use.
255+
Index next = 0;
256+
257+
// If we have a super, then we extend it, and must match its fields.
258+
// That is, we can only append fields: we cannot reorder or remove any
259+
// field that is in the super.
260+
Index numSuperFields = 0;
261+
if (super) {
262+
// We have visited the super before. Get the information about its
263+
// fields.
264+
std::vector<Index> superIndexes;
265+
auto iter = indexesAfterRemovals.find(*super);
266+
if (iter != indexesAfterRemovals.end()) {
267+
superIndexes = iter->second;
232268
} else {
269+
// We did not store any information about the parent, because we
270+
// found nothing to optimize there. That means it is not removing or
271+
// reordering anything, so its new indexes are trivial.
272+
superIndexes = makeIdentity(super->getStruct().fields.size());
273+
}
274+
275+
numSuperFields = superIndexes.size();
276+
277+
// Fields we keep but the super removed will be handled at the end.
278+
std::vector<Index> keptFieldsNotInSuper;
279+
280+
// Go over the super fields and handle them.
281+
for (Index i = 0; i < superIndexes.size(); ++i) {
282+
auto superIndex = superIndexes[i];
283+
if (superIndex == RemovedField) {
284+
if (removableIndexes.count(i)) {
285+
// This was removed in the super, and in us as well.
286+
indexesAfterRemoval[i] = RemovedField;
287+
} else {
288+
// This was removed in the super, but we actually need it. It
289+
// must appear after all other super fields, when we get to the
290+
// proper index for that, later. That is, we are reordering.
291+
keptFieldsNotInSuper.push_back(i);
292+
}
293+
} else {
294+
// The super kept this field, so we must keep it as well.
295+
assert(!removableIndexes.count(i));
296+
// We need to keep it at the same index so we remain compatible.
297+
indexesAfterRemoval[i] = superIndex;
298+
// Update |next| to refer to the next available index. Due to
299+
// possible reordering in the parent, we may not see indexes in
300+
// order here, so just take the max at each point in time.
301+
next = std::max(next, superIndex + 1);
302+
}
303+
}
304+
305+
// Handle fields we keep but the super removed.
306+
for (auto i : keptFieldsNotInSuper) {
307+
indexesAfterRemoval[i] = next++;
308+
}
309+
}
310+
311+
// Go over the fields only defined in us, and not in any super.
312+
for (Index i = numSuperFields; i < fields.size(); ++i) {
313+
if (removableIndexes.count(i)) {
233314
indexesAfterRemoval[i] = RemovedField;
234-
skip++;
315+
} else {
316+
indexesAfterRemoval[i] = next++;
235317
}
236318
}
319+
320+
// Only store the new indexes we computed if we found something
321+
// interesting. We might not, if e.g. our parent removes fields and we
322+
// add them back in the exact order we started with. In such cases,
323+
// avoid wasting memory and also time later.
324+
if (indexesAfterRemoval != makeIdentity(indexesAfterRemoval.size())) {
325+
indexesAfterRemovals[type] = indexesAfterRemoval;
326+
}
237327
}
238328
}
239329

@@ -273,15 +363,16 @@ struct GlobalTypeOptimization : public Pass {
273363
}
274364
}
275365

276-
// Remove fields where we can.
366+
// Remove/reorder fields where we can.
277367
auto remIter = parent.indexesAfterRemovals.find(oldStructType);
278368
if (remIter != parent.indexesAfterRemovals.end()) {
279369
auto& indexesAfterRemoval = remIter->second;
280370
Index removed = 0;
371+
auto copy = newFields;
281372
for (Index i = 0; i < newFields.size(); i++) {
282373
auto newIndex = indexesAfterRemoval[i];
283374
if (newIndex != RemovedField) {
284-
newFields[newIndex] = newFields[i];
375+
newFields[newIndex] = copy[i];
285376
} else {
286377
removed++;
287378
}
@@ -347,26 +438,32 @@ struct GlobalTypeOptimization : public Pass {
347438
auto& operands = curr->operands;
348439
assert(indexesAfterRemoval.size() == operands.size());
349440

350-
// Localize things so that we can simply remove the operands we no
351-
// longer need.
441+
// Ensure any children with non-trivial effects are replaced with
442+
// local.gets, so that we can remove/reorder to our hearts' content.
352443
ChildLocalizer localizer(
353444
curr, getFunction(), *getModule(), getPassOptions());
354445
replaceCurrent(localizer.getReplacement());
355446

356-
// Remove the unneeded operands.
447+
// Remove and reorder operands.
357448
Index removed = 0;
449+
std::vector<Expression*> old(operands.begin(), operands.end());
358450
for (Index i = 0; i < operands.size(); i++) {
359451
auto newIndex = indexesAfterRemoval[i];
360452
if (newIndex != RemovedField) {
361453
assert(newIndex < operands.size());
362-
operands[newIndex] = operands[i];
454+
operands[newIndex] = old[i];
363455
} else {
364456
removed++;
365457
}
366458
}
367-
operands.resize(operands.size() - removed);
368-
// We should only get here if we did actual work.
369-
assert(removed > 0);
459+
if (removed) {
460+
operands.resize(operands.size() - removed);
461+
} else {
462+
// If we didn't remove anything then we must have reordered (or else
463+
// we have done pointless work).
464+
assert(indexesAfterRemoval !=
465+
makeIdentity(indexesAfterRemoval.size()));
466+
}
370467
}
371468

372469
void visitStructSet(StructSet* curr) {

src/wasm-type-ordering.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,6 @@ struct SupertypesFirstBase
6868
};
6969

7070
struct SupertypesFirst : SupertypesFirstBase<SupertypesFirst> {
71-
template<typename T>
72-
SupertypesFirst(const T& types) : SupertypesFirstBase(types) {}
73-
7471
std::optional<HeapType> getDeclaredSuperType(HeapType type) {
7572
return type.getDeclaredSuperType();
7673
}

0 commit comments

Comments
 (0)