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
95 changes: 59 additions & 36 deletions src/passes/GlobalStructInference.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
// comparison. But we can compare structs, so if the function references are in
// vtables, and the vtables follow the above pattern, then we can optimize.
//
// This also optimizes some related things - reads from structs created in
// globals - that benefit from the infrastructure here (see unnesting, below),
// even without this type-based approach.
//
// TODO: Only do the case with a select when shrinkLevel == 0?
//

Expand Down Expand Up @@ -203,11 +207,6 @@ struct GlobalStructInference : public Pass {
}
}

if (typeGlobals.empty()) {
// We found nothing we can optimize.
return;
}

// The above loop on typeGlobalsCopy is on an unsorted data structure, and
// that can lead to nondeterminism in typeGlobals. Sort the vectors there to
// ensure determinism.
Expand Down Expand Up @@ -328,18 +327,12 @@ struct GlobalStructInference : public Pass {

// We must ignore the case of a non-struct heap type, that is, a bottom
// type (which is all that is left after we've already ruled out
// unreachable). Such things will not be in typeGlobals, which we are
// checking now anyhow.
// unreachable).
auto heapType = type.getHeapType();
auto iter = parent.typeGlobals.find(heapType);
if (iter == parent.typeGlobals.end()) {
if (!heapType.isStruct()) {
return;
}

// This cannot be a bottom type as we found it in the typeGlobals map,
// which only contains types of struct.news.
assert(heapType.isStruct());

// The field must be immutable.
std::optional<Field> field;
if (fieldIndex != DescriptorIndex) {
Expand All @@ -349,12 +342,36 @@ struct GlobalStructInference : public Pass {
}
}

auto& wasm = *getModule();

// This is a read of an immutable field. See if it is a trivial case, of
// a read from an immutable global.
if (auto* get = ref->dynCast<GlobalGet>()) {
auto* global = wasm.getGlobal(get->name);
if (!global->mutable_ && !global->imported()) {
if (auto* structNew = global->init->dynCast<StructNew>()) {
auto value = readFromStructNew(structNew, fieldIndex, field);
// TODO: handle non-constant values using unnesting, as below.
if (value.isConstant()) {
replaceCurrent(value.getConstant().makeExpression(wasm));
return;
}
// Otherwise, continue to try the main type-based optimization
// below.
}
}
}

auto iter = parent.typeGlobals.find(heapType);
if (iter == parent.typeGlobals.end()) {
return;
}

const auto& globals = iter->second;
if (globals.size() == 0) {
return;
}

auto& wasm = *getModule();
Builder builder(wasm);

if (globals.size() == 1) {
Expand Down Expand Up @@ -388,28 +405,8 @@ struct GlobalStructInference : public Pass {
for (Index i = 0; i < globals.size(); i++) {
Name global = globals[i];
auto* structNew = wasm.getGlobal(global)->init->cast<StructNew>();
// The value that is read from this struct.new.
Value value;

// Find the value read from the struct and represent it as a Value.
PossibleConstantValues constant;
if (field && structNew->isWithDefault()) {
constant.note(Literal::makeZero(field->type));
value.content = constant;
} else {
Expression* operand;
if (field) {
operand = structNew->operands[fieldIndex];
} else {
operand = structNew->desc;
}
constant.note(operand, wasm);
if (constant.isConstant()) {
value.content = constant;
} else {
value.content = operand;
}
}
// Find the value read from the struct.new.
auto value = readFromStructNew(structNew, fieldIndex, field);

// If the value is constant, it may be grouped as mentioned before.
// See if it matches anything we've seen before.
Expand Down Expand Up @@ -533,6 +530,32 @@ struct GlobalStructInference : public Pass {
ReFinalize().walkFunctionInModule(func, getModule());
}
}

Value readFromStructNew(StructNew* structNew,
Index fieldIndex,
std::optional<Field>& field) {
// Find the value read from the struct and represent it as a Value.
Value value;
PossibleConstantValues constant;
if (field && structNew->isWithDefault()) {
constant.note(Literal::makeZero(field->type));
value.content = constant;
} else {
Expression* operand;
if (field) {
operand = structNew->operands[fieldIndex];
} else {
operand = structNew->desc;
}
constant.note(operand, *getModule());
if (constant.isConstant()) {
value.content = constant;
} else {
value.content = operand;
}
}
return value;
}
};

// Find the optimization opportunitites in parallel.
Expand Down
206 changes: 206 additions & 0 deletions test/lit/passes/gsi-nontype.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
;; RUN: foreach %s %t wasm-opt --gsi -all --closed-world -S -o - | filecheck %s

;; Non-type-based optimizations in --gsi

;; Create an immutable vtable in an immutable global, which we can optimize
;; with.
(module
;; CHECK: (type $vtable (struct (field funcref)))
(type $vtable (struct funcref))

;; CHECK: (type $1 (func))

;; CHECK: (import "a" "b" (global $imported funcref))
(import "a" "b" (global $imported funcref))

;; CHECK: (global $vtable (ref $vtable) (struct.new $vtable
;; CHECK-NEXT: (global.get $imported)
;; CHECK-NEXT: ))
(global $vtable (ref $vtable)
(struct.new $vtable
(global.get $imported)
)
Comment on lines +21 to +23
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.

It would be neat (and not too hard, I think) to expand this to support arbitrarily deep paths through struct.new, array.new, and global.get expressions in global initializers.

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, that is what the TODO about unnesting is about. (I think we need to unnest, like the other code here? Unless there is a simpler way I am missing.)

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.

It would be fairly straightforward to have a utility that is given the root of some sequence of accessors (kind of like a gep in LLVM, except not all rolled into one instruction) and then follows the access path as long as it is known to remain constant, returning (a pointer to) the result.

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.

Would that be useful anywhere else, do you think?

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.

Hmm, I'd have to think about that. I saw that meanwhile you uploaded a solution that unnests the intermediate values into globals. The only benefit a path-walking utility would have over that would be fewer new globals to be optimized back out later. A downside would be potentially quadratic behavior, though.

)

;; CHECK: (func $test (type $1)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (global.get $imported)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $test
;; This get reads $import.
(drop
(struct.get $vtable 0
(global.get $vtable)
)
)
)
)

;; As above, but the global is not immutable, so we cannot optimize.
(module
;; CHECK: (type $vtable (struct (field funcref)))
(type $vtable (struct funcref))

;; CHECK: (type $1 (func))

;; CHECK: (import "a" "b" (global $imported funcref))
(import "a" "b" (global $imported funcref))

;; CHECK: (global $vtable (mut (ref $vtable)) (struct.new $vtable
;; CHECK-NEXT: (global.get $imported)
;; CHECK-NEXT: ))
(global $vtable (mut (ref $vtable))
(struct.new $vtable
(global.get $imported)
)
)

;; CHECK: (func $test (type $1)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (struct.get $vtable 0
;; CHECK-NEXT: (global.get $vtable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $test
(drop
(struct.get $vtable 0
(global.get $vtable)
)
)
)
)

;; As above, but the global does not contain a struct.new, so we cannot
;; optimize.
(module
;; CHECK: (type $vtable (struct (field funcref)))
(type $vtable (struct funcref))

;; CHECK: (type $1 (func))

;; CHECK: (import "a" "b" (global $imported funcref))
(import "a" "b" (global $imported funcref))

;; CHECK: (global $vtable (ref null $vtable) (ref.null none))
(global $vtable (ref null $vtable) (ref.null $vtable))

;; CHECK: (func $test (type $1)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (struct.get $vtable 0
;; CHECK-NEXT: (global.get $vtable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $test
(drop
(struct.get $vtable 0
(global.get $vtable)
)
)
)
)

;; As above, but the value in the struct.new is not constant, so we cannot
;; optimize.
(module
;; CHECK: (type $table (struct (field anyref)))
(type $table (struct anyref))

;; CHECK: (type $1 (func))

;; CHECK: (import "a" "b" (global $imported funcref))
(import "a" "b" (global $imported funcref))

;; CHECK: (global $table (ref $table) (struct.new $table
;; CHECK-NEXT: (struct.new_default $table)
;; CHECK-NEXT: ))
(global $table (ref $table)
(struct.new $table
(struct.new_default $table)
)
)

;; CHECK: (func $test (type $1)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (struct.get $table 0
;; CHECK-NEXT: (global.get $table)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $test
(drop
(struct.get $table 0
(global.get $table)
)
)
)
)

;; Test we can optimize a descriptor read.
(module
(rec
;; CHECK: (rec
;; CHECK-NEXT: (type $struct (sub (descriptor $desc (struct))))
(type $struct (sub (descriptor $desc (struct))))
;; CHECK: (type $desc (sub (describes $struct (struct))))
(type $desc (sub (describes $struct (struct))))
)

;; CHECK: (type $2 (func))

;; CHECK: (import "a" "b" (global $imported (ref (exact $desc))))
(import "a" "b" (global $imported (ref (exact $desc))))

;; CHECK: (global $struct (ref $struct) (struct.new_default $struct
;; CHECK-NEXT: (global.get $imported)
;; CHECK-NEXT: ))
(global $struct (ref $struct)
(struct.new $struct
(global.get $imported)
)
)

;; CHECK: (func $test (type $2)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (global.get $imported)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $test
;; This get reads $import.
(drop
(ref.get_desc $struct
(global.get $struct)
)
)
)
)

;; Check we do not error on a global.get of an imported global.
(module
;; CHECK: (type $vtable (struct (field funcref)))
(type $vtable (struct funcref))

;; CHECK: (type $1 (func))

;; CHECK: (import "a" "b" (global $imported (ref $vtable)))
(import "a" "b" (global $imported (ref $vtable)))

;; CHECK: (func $test (type $1)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (struct.get $vtable 0
;; CHECK-NEXT: (global.get $imported)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $test
(drop
(struct.get $vtable 0
(global.get $imported)
)
)
)
)

10 changes: 5 additions & 5 deletions test/lit/passes/gsi.wast
Original file line number Diff line number Diff line change
Expand Up @@ -1901,7 +1901,7 @@
;; CHECK: (type $struct (struct (field i8)))
(type $struct (struct (field i8)))

;; CHECK: (type $1 (func (result i32)))
;; CHECK: (type $1 (func (param (ref $struct)) (result i32)))

;; CHECK: (global $A (ref $struct) (struct.new $struct
;; CHECK-NEXT: (i32.const 257)
Expand All @@ -1917,7 +1917,7 @@
(i32.const 258)
))

;; CHECK: (func $test (type $1) (result i32)
;; CHECK: (func $test (type $1) (param $x (ref $struct)) (result i32)
;; CHECK-NEXT: (select
;; CHECK-NEXT: (i32.and
;; CHECK-NEXT: (i32.const 257)
Expand All @@ -1929,18 +1929,18 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: (ref.eq
;; CHECK-NEXT: (ref.as_non_null
;; CHECK-NEXT: (global.get $A)
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: (global.get $A)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $test (result i32)
(func $test (param $x (ref $struct)) (result i32)
;; We can infer this value is one of two things since only two objects exist
;; of this type. We must emit the proper truncated value for them, as the
;; values are truncated into i8.
(struct.get_u $struct 0
(global.get $A)
(local.get $x)
)
)
)
Expand Down
Loading