Skip to content
Merged
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
275 changes: 41 additions & 234 deletions src/passes/GlobalTypeOptimization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

#include "ir/eh-utils.h"
#include "ir/localize.h"
#include "ir/module-utils.h"
#include "ir/names.h"
#include "ir/ordering.h"
#include "ir/struct-utils.h"
Expand All @@ -42,10 +41,8 @@ namespace {

// Information about usage of a field.
struct FieldInfo {
// This represents a normal write for normal fields. For a descriptor, we only
// note "dangerous" writes, specifically ones which might trap (when the
// descriptor in a struct.new is nullable), which is a special situation we
// must avoid.
// This represents a normal write for normal fields. (Unused descriptors are
// optimized in Unsubtyping instead.)
bool hasWrite = false;
bool hasRead = false;

Expand Down Expand Up @@ -87,9 +84,8 @@ struct FieldInfoScanner
HeapType type,
Index index,
FieldInfo& info) {
if (index == StructUtils::DescriptorIndex && expr->type.isNonNullable()) {
// A non-dangerous write to a descriptor, which as mentioned above, we do
// not track.
if (index == StructUtils::DescriptorIndex) {
// We do not optimize descriptors. Ignore them.
return;
}
info.noteWrite();
Expand All @@ -105,6 +101,9 @@ struct FieldInfoScanner
}

void noteRead(HeapType type, Index index, FieldInfo& info) {
if (index == StructUtils::DescriptorIndex) {
return;
}
info.noteRead();
}

Expand Down Expand Up @@ -133,15 +132,6 @@ struct GlobalTypeOptimization : public Pass {
static const Index RemovedField = Index(-1);
std::unordered_map<HeapType, std::vector<Index>> indexesAfterRemovals;

// The types that no longer need a descriptor.
std::unordered_set<HeapType> haveUnneededDescriptors;

// Descriptor types that are not needed by their described types but that
// still need to be descriptors for their own subtypes and supertypes to be
// valid. We will keep them descriptors by having them describe trivial new
// placeholder types.
std::unordered_set<HeapType> descriptorsOfPlaceholders;

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

auto trapsNeverHappen = getPassOptions().trapsNeverHappen;

// Find and analyze struct operations inside each function.
StructUtils::FunctionStructValuesMap<FieldInfo> functionNewInfos(*module),
functionSetGetInfos(*module);
Expand All @@ -163,23 +151,6 @@ struct GlobalTypeOptimization : public Pass {
// Combine the data from the functions.
functionSetGetInfos.combineInto(combinedSetGetInfos);

// Custom descriptor handling: We need to look at struct.news, which
// normally we ignore (nothing in a struct.new can cause fields to remain
// mutable, or force the field to stay around. We cannot ignore them with CD
// because struct.news can now trap, and removing the descriptor could
// change things, so we must be careful. (Without traps, though, this is
// unnecessary.)
if (module->features.hasCustomDescriptors() && !trapsNeverHappen) {
for (auto& [func, infos] : functionNewInfos) {
for (auto& [type, info] : infos) {
if (info.desc.hasWrite) {
// Copy the descriptor write to the info we will propagate below.
combinedSetGetInfos[type].desc.noteWrite();
}
}
}
}

// Propagate information to super and subtypes on set/get infos:
//
// * For removing unread fields, we can only remove a field if it is never
Expand Down Expand Up @@ -408,127 +379,29 @@ struct GlobalTypeOptimization : public Pass {
indexesAfterRemovals[type] = indexesAfterRemoval;
}
}

// Process the descriptor.
if (auto desc = type.getDescriptorType()) {
// To remove a descriptor, it must not be read via supertypes and it
// must not have to remain in supertypes because e.g. they are public.
// It must also have no write (see above, we note only dangerous writes
// which might trap), as if it could trap, we'd have no easy way to
// remove it in a global scope.
// TODO: We could check and handle the global scope specifically, but
// the trapsNeverHappen flag avoids this problem entirely anyhow.
//
// This does not handle descriptor subtyping, see below.
auto super = type.getDeclaredSuperType();
bool superNeedsDescriptor = super && super->getDescriptorType() &&
!haveUnneededDescriptors.count(*super);
bool descriptorIsUsed =
dataFromSupers.desc.hasRead ||
(dataFromSupers.desc.hasWrite && !trapsNeverHappen);
if (!superNeedsDescriptor && !descriptorIsUsed) {
haveUnneededDescriptors.insert(type);
}
}
}

// Handle descriptor subtyping:
//
// A -> A.desc
// ^
// B -> B.desc
//
// Say we want to optimize A to no longer have a descriptor. Then A.desc
// will no longer describe A. But A.desc still needs to be a descriptor for
// it to remain a valid supertype of B.desc. To allow the optimization of A
// to proceed, we will introduce a placeholder type for A.desc to describe,
// keeping it a descriptor type.
if (!haveUnneededDescriptors.empty()) {
StructUtils::TypeHierarchyPropagator<StructUtils::CombinableBool>
descPropagator(subTypes);

// Populate the initial data: Any descriptor we did not see was unneeded,
// is needed.
StructUtils::TypeHierarchyPropagator<
StructUtils::CombinableBool>::StructMap remainingDesciptors;
for (auto type : subTypes.types) {
if (auto desc = type.getDescriptorType()) {
if (!haveUnneededDescriptors.count(type)) {
// This descriptor type is needed.
remainingDesciptors[*desc].value = true;
}
}
}

// Propagate.
descPropagator.propagateToSuperAndSubTypes(remainingDesciptors);

// Determine the set of descriptor types that will need placeholder
// describees.
for (auto [type, kept] : remainingDesciptors) {
if (kept.value) {
auto desc = type.getDescribedType();
assert(desc);
if (haveUnneededDescriptors.count(*desc)) {
descriptorsOfPlaceholders.insert(type);
}
}
}
}

// If we found things that can be removed, remove them from instructions.
// (Note that we must do this first, while we still have the old heap types
// that we can identify, and only after this should we update all the types
// throughout the module.)
if (!indexesAfterRemovals.empty() || !haveUnneededDescriptors.empty()) {
if (!indexesAfterRemovals.empty()) {
updateInstructions(*module);
}

// Update the types in the entire module.
if (!indexesAfterRemovals.empty() || !canBecomeImmutable.empty() ||
!haveUnneededDescriptors.empty()) {
if (!indexesAfterRemovals.empty() || !canBecomeImmutable.empty()) {
updateTypes(*module);
}
}

void updateTypes(Module& wasm) {
class TypeRewriter : public GlobalTypeRewriter {
GlobalTypeOptimization& parent;
// Differentiate the first occurrence of a descriptor of a placeholder
// type, which is actually saving space for the placeholder itself, and
// the next occurrence, which is real.
bool sawPlaceholder = false;

public:
TypeRewriter(Module& wasm, GlobalTypeOptimization& parent)
: GlobalTypeRewriter(wasm), parent(parent) {}

std::vector<HeapType> getSortedTypes(PredecessorGraph preds) override {
auto types = GlobalTypeRewriter::getSortedTypes(std::move(preds));
// Intersperse placeholder types throughout the sorted types. Each
// descriptor type that needs a placeholder describee will be duplicated
// in the list of sorted types. The first appearance will be modified to
// become the placeholder describee and the subsequent appearance will
// be modified to describe the preceding placeholder. We represent the
// placeholder slots here using their intended descriptor types for two
// reasons: first, it lets us easily recognize the placeholder slots
// in modifyTypeBuilderEntry. Second, it ensures that the type-to-index
// mapping created in GlobalTypeRewriter::rebuiltTypes does not end up
// mapping the placeholder types to any index, since their mappings are
// immediately overwritten by the following "real" occurrences of the
// descriptor types.
std::vector<HeapType> typesWithPlaceholders;
for (auto type : types) {
if (parent.descriptorsOfPlaceholders.count(type)) {
// Insert the type an extra time to make space for the placeholder.
typesWithPlaceholders.push_back(type);
}
typesWithPlaceholders.push_back(type);
}

return typesWithPlaceholders;
}

void modifyStruct(HeapType oldStructType, Struct& struct_) override {
auto& newFields = struct_.fields;

Expand Down Expand Up @@ -581,45 +454,6 @@ struct GlobalTypeOptimization : public Pass {
}
}
}

void modifyTypeBuilderEntry(TypeBuilder& typeBuilder,
Index i,
HeapType oldType) override {
if (!oldType.isStruct()) {
return;
}

// Remove an unneeded descriptor.
if (parent.haveUnneededDescriptors.count(oldType)) {
typeBuilder[i].descriptor(std::nullopt);
}

// Remove an unneeded describee or describe a placeholder type.
if (auto described = oldType.getDescribedType()) {
if (parent.haveUnneededDescriptors.count(*described)) {
if (parent.descriptorsOfPlaceholders.count(oldType)) {
if (!sawPlaceholder) {
// This is the placeholder describee. Set its descriptor to be
// the succeeding real descriptor type.
typeBuilder[i] = Struct{};
typeBuilder[i].setShared(described->getShared());
typeBuilder[i].descriptor(typeBuilder[i + 1]);
typeBuilder[i].describes(std::nullopt);
typeBuilder[i].subTypeOf(std::nullopt);
sawPlaceholder = true;
} else {
// This is the real placedholder-describing descriptor type.
// Have it describe the preceding placeholder describee.
typeBuilder[i].describes(typeBuilder[i - 1]);
sawPlaceholder = false;
}
} else {
// This type no longer needs its describee.
typeBuilder[i].describes(std::nullopt);
}
}
}
}
};

TypeRewriter(wasm, *this).update();
Expand Down Expand Up @@ -650,20 +484,18 @@ struct GlobalTypeOptimization : public Pass {
if (curr->type == Type::unreachable) {
return;
}
if (curr->isWithDefault()) {
// No indices to remove.
return;
}

auto type = curr->type.getHeapType();
auto removeDesc = parent.haveUnneededDescriptors.count(type);

// There may be no indexes to remove, if we are only removing the
// descriptor.
std::vector<Index>* indexesAfterRemoval = nullptr;
// There are also no indexes to remove if we only write default values.
if (!curr->isWithDefault()) {
auto iter = parent.indexesAfterRemovals.find(type);
if (iter != parent.indexesAfterRemovals.end()) {
indexesAfterRemoval = &iter->second;
}

auto iter = parent.indexesAfterRemovals.find(type);
if (iter == parent.indexesAfterRemovals.end()) {
return;
}
std::vector<Index>& indexesAfterRemoval = iter->second;

// Ensure any children with non-trivial effects are replaced with
// local.gets, so that we can remove/reorder to our hearts' content.
Expand All @@ -678,45 +510,32 @@ struct GlobalTypeOptimization : public Pass {
needEHFixups = true;
}

if (indexesAfterRemoval) {
// Remove and reorder operands.
auto& operands = curr->operands;
assert(indexesAfterRemoval->size() == operands.size());

Index removed = 0;
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 (!func &&
EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) {
removedTrappingInits.push_back(old[i]);
}
}
}
if (removed) {
operands.resize(operands.size() - removed);
// Remove and reorder operands.
auto& operands = curr->operands;
assert(indexesAfterRemoval.size() == operands.size());

Index removed = 0;
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 {
// If we didn't remove anything then we must have reordered (or else
// we have done pointless work).
assert(*indexesAfterRemoval !=
makeIdentity(indexesAfterRemoval->size()));
++removed;
if (!func &&
EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) {
removedTrappingInits.push_back(old[i]);
}
}
}

if (removeDesc) {
// We already handled the case of a possible trap here, so we can
// remove the descriptor, but must be careful of nested effects (our
// descriptor may be ok to remove, but a nested struct.new may not).
if (!func &&
EffectAnalyzer(getPassOptions(), *getModule(), curr->desc).trap) {
removedTrappingInits.push_back(curr->desc);
}
curr->desc = nullptr;
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).
assert(indexesAfterRemoval !=
makeIdentity(indexesAfterRemoval.size()));
}
}

Expand Down Expand Up @@ -761,18 +580,6 @@ struct GlobalTypeOptimization : public Pass {
curr->index = newIndex;
}

void visitRefCast(RefCast* curr) {
// Unreachable ref.cast_desc instructions would not have been counted as
// reading the descriptor field, so their descriptor operands may no
// longer be descriptors. This is invalid, so replace such casts
// entirely.
if (curr->type == Type::unreachable && curr->desc &&
curr->desc->type != Type::unreachable) {
assert(curr->ref->type == Type::unreachable);
replaceCurrent(curr->ref);
}
}

void visitFunction(Function* curr) {
if (needEHFixups) {
EHUtils::handleBlockNestedPops(curr, *getModule());
Expand Down
Loading
Loading