Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions src/ir/js-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@

namespace wasm::JSUtils {

// Whether a field is immutable and a reference to a subtype of externref that
// could hold a JS prototype.
inline bool isPossibleJSPrototypeField(const Field& field) {
if (field.mutable_ != Immutable) {
return false;
}
if (!field.type.isRef()) {
return false;
}
return field.type.getHeapType().isMaybeShared(HeapType::ext);
}

// Whether this is a descriptor struct type whose first field is immutable and a
// subtype of externref.
inline bool hasPossibleJSPrototypeField(HeapType type) {
Expand All @@ -34,13 +46,7 @@ inline bool hasPossibleJSPrototypeField(HeapType type) {
if (fields.empty()) {
return false;
}
if (fields[0].mutable_ == Mutable) {
return false;
}
if (!fields[0].type.isRef()) {
return false;
}
return fields[0].type.getHeapType().isMaybeShared(HeapType::ext);
return isPossibleJSPrototypeField(fields[0]);
}

// Calls flowIn and flowOut on all types that may flow in from or out to JS.
Expand Down
200 changes: 154 additions & 46 deletions src/passes/GlobalTypeOptimization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,20 @@ struct FieldInfo {

struct FieldInfoScanner
: public StructUtils::StructScanner<FieldInfo, FieldInfoScanner> {
std::unordered_map<Function*, std::vector<Type>>& jsExposedTypes;

std::unique_ptr<Pass> create() override {
return std::make_unique<FieldInfoScanner>(functionNewInfos,
functionSetGetInfos);
return std::make_unique<FieldInfoScanner>(
functionNewInfos, functionSetGetInfos, jsExposedTypes);
}

FieldInfoScanner(
StructUtils::FunctionStructValuesMap<FieldInfo>& functionNewInfos,
StructUtils::FunctionStructValuesMap<FieldInfo>& functionSetGetInfos)
StructUtils::FunctionStructValuesMap<FieldInfo>& functionSetGetInfos,
std::unordered_map<Function*, std::vector<Type>>& jsExposedTypes)
: StructUtils::StructScanner<FieldInfo, FieldInfoScanner>(
functionNewInfos, functionSetGetInfos) {}
functionNewInfos, functionSetGetInfos),
jsExposedTypes(jsExposedTypes) {}

void noteExpression(Expression* expr,
HeapType type,
Expand Down Expand Up @@ -116,16 +120,8 @@ struct FieldInfoScanner
// Converting a reference to externref makes the prototype field on its
// descriptor available to be read by JS, if such a field exists.
void visitRefAs(RefAs* curr) {
if (curr->op != ExternConvertAny) {
return;
}
if (!curr->value->type.isRef()) {
return;
}
if (auto desc = curr->value->type.getHeapType().getDescriptorType();
desc && JSUtils::hasPossibleJSPrototypeField(*desc)) {
auto exact = curr->value->type.getExactness();
functionSetGetInfos[getFunction()][{*desc, exact}][0].noteRead();
if (curr->op == ExternConvertAny && curr->value->type.isRef()) {
jsExposedTypes.at(getFunction()).push_back(curr->value->type);
}
}
};
Expand All @@ -139,6 +135,11 @@ struct GlobalTypeOptimization : public Pass {
// rare).
std::unordered_map<HeapType, std::vector<bool>> canBecomeImmutable;

// Descriptor types that are exposed to JS but do _not_ configure prototypes
// for their described types. We must avoid optimizing these types such that
// they start configuring prototypes.
std::unordered_set<HeapType> exposedNoProtoDescs;

// Maps each field to its new index after field removals. That is, this
// takes into account that fields before this one may have been removed,
// which would then reduce this field's index. If a field itself is removed,
Expand All @@ -149,6 +150,32 @@ struct GlobalTypeOptimization : public Pass {
static const Index RemovedField = Index(-1);
std::unordered_map<HeapType, std::vector<Index>> indexesAfterRemovals;

struct IndexAnalysis {
// The size after removing fields and possibly adding a placeholder.
Index newSize = 0;
bool hasPlaceholder = false;

// `indexes` is the mapping from old to new indices.
IndexAnalysis(const std::vector<Index>& indexes) {
Index maxIndex = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add some comments to this class and/or properties? Just reading the code, I'd expect newSize to mean the size after removals - is that right? And the input is the mapping of old indexes to new indexes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, newSize is the size after removals and possible placeholder insertion. And yes, the input is the old to new mapping. I'll add comments.

bool hasKept = false;
bool hasIndexZero = false;
for (auto idx : indexes) {
if (idx != RemovedField) {
hasKept = true;
maxIndex = std::max(maxIndex, idx);
if (idx == 0) {
hasIndexZero = true;
}
}
}
newSize = hasKept ? maxIndex + 1 : 0;
// We know there is a placeholder if we have fields but none of them are
// at index 0.
hasPlaceholder = hasKept && !hasIndexZero;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit "magical" - worth a comment

}
};

void run(Module* module) override {
if (!module->features.hasGC()) {
return;
Expand All @@ -157,21 +184,32 @@ struct GlobalTypeOptimization : public Pass {
Fatal() << "GTO requires --closed-world";
}

std::unordered_map<Function*, std::vector<Type>> jsExposedTypesByFunction;
jsExposedTypesByFunction[nullptr];
for (auto& func : module->functions) {
jsExposedTypesByFunction[func.get()];
}

// Find and analyze struct operations inside each function.
StructUtils::FunctionStructValuesMap<FieldInfo> functionNewInfos(*module),
functionSetGetInfos(*module);
FieldInfoScanner scanner(functionNewInfos, functionSetGetInfos);
FieldInfoScanner scanner(
functionNewInfos, functionSetGetInfos, jsExposedTypesByFunction);
scanner.run(getPassRunner(), module);
scanner.runOnModuleCode(getPassRunner(), module);

// Combine the data from the functions.
functionSetGetInfos.combineInto(combinedSetGetInfos);
std::vector<Type> jsExposedTypes;
for (auto& [_, types] : jsExposedTypesByFunction) {
jsExposedTypes.insert(jsExposedTypes.end(), types.begin(), types.end());
}

SubTypes subTypes(*module);

// Analyze the JS interface to find fields holding configured prototypes
// that cannot be removed.
analyzeJSInterface(*module, subTypes);
analyzeJSInterface(*module, subTypes, jsExposedTypes);

// Propagate information to super and subtypes on set/get infos:
//
Expand Down Expand Up @@ -291,15 +329,18 @@ struct GlobalTypeOptimization : public Pass {
}

// We need to compute the new set of indexes if we are removing fields, or
// if our parent removed fields. In the latter case, our parent may have
// reordered fields even if we ourselves are not removing anything, and we
// must update to match the parent's order.
// if our parent removed fields, or if we might need a placeholder because
// this type is exposed outside the module and does not configure a JS
// prototype. If we have a parent, it may have reordered fields even if we
// ourselves are not removing anything, and we must update to match the
// parent's order.
auto super = type.getDeclaredSuperType();
auto superHasUpdates = super && indexesAfterRemovals.contains(*super);
if (!removableIndexes.empty() || superHasUpdates) {
// We are removing fields. Reorder them to allow that, as in the general
// case we can only remove fields from the end, so that if our subtypes
// still need the fields they can append them. For example:
bool isExposedNoProto = exposedNoProtoDescs.contains(type);
if (!removableIndexes.empty() || superHasUpdates || isExposedNoProto) {
// We might be removing fields. Reorder them to allow that, as in the
// general case we can only remove fields from the end, so that if our
// subtypes still need the fields they can append them. For example:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please comment about the importance of isExposedNoProto to the placeholder issue

//
// type A = { x: i32, y: f64 };
// type B : A = { x: 132, y: f64, z: v128 };
Expand Down Expand Up @@ -392,6 +433,38 @@ struct GlobalTypeOptimization : public Pass {
}
}

// If the type has no supertype (or its supertype has no fields), check
// if its first field becomes prototype-exposing. If so, add a
// placeholder at index 0 and shift all computed indices.
if (isExposedNoProto && (!super || super->getStruct().fields.empty())) {
// Find the field that will become field 0.
Index i = 0;
for (; i < fields.size(); ++i) {
if (indexesAfterRemoval[i] == 0) {
break;
}
}
// Check whether that field would expose a prototype.
if (i < fields.size()) {
Field optimizedField = fields[i];
if (auto it = canBecomeImmutable.find(type);
it != canBecomeImmutable.end() && i < it->second.size() &&
it->second[i]) {
optimizedField.mutable_ = Immutable;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we apply immutability here? That is, why does this duplicate the normal code that turns fields immutable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check if the normal optimization would produce an immutable externref in the first field without our placeholder intervention. If we didn't check what normal optimization would do with immutability here, then we would either end up adding unnecessary placeholders when the first field would become a mutable externref or end up missing necessary placeholders when the first field is optimized from mutable to immutable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we do this in that normal code, then?

I mean that it seems odd to have two places in the code that turn things immutable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how to deduplicate it. Here we are computing the new field indices, and to do that we need to know whether we are going to need a placeholder, which means we need to know what is going to happen to the first field. Only later, after we have the new indices, do we actually materialize the new fields.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping we could compute new-immutability in one place and use it here, rather than computing it here, but yeah, maybe there isn't a good way to do that, I don't see one either.

}
if (JSUtils::isPossibleJSPrototypeField(optimizedField)) {
// The field exposes a prototype. Increment all field indices to
// make room for a placeholder first field (which will be
// materialized as an i8 field later).
for (auto& idx : indexesAfterRemoval) {
if (idx != RemovedField) {
++idx;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment that we do not add the i8 here, we just make room for it, and that room - the missing index 0 - is the marker we use to identify the need to add the i8 later (is that right?)

}
}
}
}

// Only store the new indexes we computed if we found something
// interesting. We might not, if e.g. our parent removes fields and we
// add them back in the exact order we started with. In such cases,
Expand All @@ -416,7 +489,9 @@ struct GlobalTypeOptimization : public Pass {
}
}

void analyzeJSInterface(Module& wasm, const SubTypes& subTypes) {
void analyzeJSInterface(Module& wasm,
const SubTypes& subTypes,
const std::vector<Type>& jsExposedTypes) {
if (!wasm.features.hasCustomDescriptors()) {
return;
}
Expand All @@ -426,10 +501,16 @@ struct GlobalTypeOptimization : public Pass {
// Mark the relevant prototype field as read and return true iff we newly
// know we have to propagate the exposure to subtypes.
auto noteExposed = [&](HeapType type, Exactness exact = Inexact) -> bool {
if (auto desc = type.getDescriptorType();
desc && JSUtils::hasPossibleJSPrototypeField(*desc)) {
// This field holds a JS-visible prototype. Do not remove it.
combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead();
if (auto desc = type.getDescriptorType()) {
if (JSUtils::hasPossibleJSPrototypeField(*desc)) {
// This descriptor configures a JS-visible prototype. Do not remove
// it.
combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead();
} else {
// This descriptor does _not_ configure a JS prototype. Do not add
// one.
exposedNoProtoDescs.insert(*desc);
}
}
if (exact == Inexact) {
return subtypesExposed.insert(type).second;
Expand All @@ -449,6 +530,12 @@ struct GlobalTypeOptimization : public Pass {

JSUtils::iterJSInterface(wasm, flowIn, flowOut);

for (auto type : jsExposedTypes) {
if (type.isRef()) {
noteExposed(type.getHeapType(), type.getExactness());
}
}

// Any type that is a subtype of an exposed type is also exposed. Propagate
// from supertypes to subtypes.
std::vector<HeapType> work(subtypesExposed.begin(), subtypesExposed.end());
Expand All @@ -471,6 +558,20 @@ struct GlobalTypeOptimization : public Pass {
}
}
}

// Also propagate lack of exposed descriptors to supertypes so that
// descriptor hierarchies have consistent layouts. Do not propagate to
// supertypes that actually expose a prototype, which can happen when the
// subtype has refined an externref field to a nullexternref.
for (auto type : subTypes.types) {
if (exposedNoProtoDescs.contains(type)) {
auto curr = type.getDeclaredSuperType();
while (curr && !JSUtils::hasPossibleJSPrototypeField(*curr)) {
exposedNoProtoDescs.insert(*curr);
curr = curr->getDeclaredSuperType();
}
}
}
}

void updateTypes(Module& wasm) {
Expand Down Expand Up @@ -499,17 +600,19 @@ struct GlobalTypeOptimization : public Pass {
auto remIter = parent.indexesAfterRemovals.find(oldStructType);
if (remIter != parent.indexesAfterRemovals.end()) {
auto& indexesAfterRemoval = remIter->second;
Index removed = 0;
IndexAnalysis analysis(indexesAfterRemoval);
auto copy = newFields;
for (Index i = 0; i < newFields.size(); i++) {
newFields.resize(analysis.newSize);
if (analysis.hasPlaceholder) {
newFields[0] = Field(Field::i8, Immutable);
}
for (Index i = 0; i < copy.size(); i++) {
auto newIndex = indexesAfterRemoval[i];
if (newIndex != RemovedField) {
assert(newIndex < newFields.size());
newFields[newIndex] = copy[i];
} else {
removed++;
}
}
newFields.resize(newFields.size() - removed);

// Update field names as well. The Type Rewriter cannot do this for
// us, as it does not know which old fields map to which new ones (it
Expand Down Expand Up @@ -595,26 +698,31 @@ struct GlobalTypeOptimization : public Pass {
auto& operands = curr->operands;
assert(indexesAfterRemoval.size() == operands.size());

Index removed = 0;
IndexAnalysis analysis(indexesAfterRemoval);
std::vector<Expression*> old(operands.begin(), operands.end());
for (Index i = 0; i < operands.size(); ++i) {
auto newIndex = indexesAfterRemoval[i];
if (newIndex != RemovedField) {
assert(newIndex < operands.size());
operands[newIndex] = old[i];
} else {
++removed;
if (indexesAfterRemoval[i] == RemovedField) {
if (!func &&
EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) {
removedTrappingInits.push_back(old[i]);
}
}
}
if (removed) {
operands.resize(operands.size() - removed);
} else {
// If we didn't remove anything then we must have reordered (or else
// we have done pointless work).
operands.resize(analysis.newSize);
if (analysis.hasPlaceholder) {
// The value we put in the i8 placeholder does not matter.
operands[0] = Builder(*getModule()).makeConst(Literal(int32_t(0)));
Comment thread
kripken marked this conversation as resolved.
}
for (Index i = 0; i < old.size(); ++i) {
auto newIndex = indexesAfterRemoval[i];
if (newIndex != RemovedField) {
assert(newIndex < operands.size());
operands[newIndex] = old[i];
}
}
if (analysis.newSize == old.size() && !analysis.hasPlaceholder) {
// If we didn't remove or insert anything then we must have reordered
// (or else we have done pointless work).
assert(indexesAfterRemoval !=
makeIdentity(indexesAfterRemoval.size()));
}
Expand Down Expand Up @@ -697,7 +805,7 @@ struct GlobalTypeOptimization : public Pass {
}
auto& indexesAfterRemoval = iter->second;
auto newIndex = indexesAfterRemoval[index];
assert(newIndex < indexesAfterRemoval.size() ||
assert(newIndex < IndexAnalysis(indexesAfterRemoval).newSize ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this making us slower, to recompute this? Should we store it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IndexAnalysis is used in three places: here in this assertion, when updating struct types, and when updating struct.new instructions. For the other two places, we're doing work linear in the number of fields already, so I doubt there's a problem. For this usage, I'd like to say it's not worth adding any caching of results because it's just in an assertion.

newIndex == RemovedField);
return newIndex;
}
Expand Down
Loading