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
61 changes: 51 additions & 10 deletions src/passes/OptimizeInstructions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,7 @@ struct OptimizeInstructions
}

void visitCallRef(CallRef* curr) {
skipNonNullCast(curr->target);
skipNonNullCast(curr->target, curr);
if (trapOnNull(curr, curr->target)) {
return;
}
Expand Down Expand Up @@ -1473,10 +1473,51 @@ struct OptimizeInstructions
// See "notes on removing casts", above. However, in most cases removing a
// non-null cast is obviously safe to do, since we only remove one if another
// check will happen later.
void skipNonNullCast(Expression*& input) {
//
// We also pass in the parent, because we need to be careful about ordering:
// if the parent has other children than |input| then we may not be able to
// remove the trap. For example,
//
// (struct.set
// (ref.as_non_null X)
// (call $foo)
// )
//
// If X is null we'd trap before the call to $foo. If we remove the
// ref.as_non_null then the struct.set will still trap, of course, but that
// will only happen *after* the call, which is wrong.
void skipNonNullCast(Expression*& input, Expression* parent) {
// Check the other children for the ordering problem only if we find a
// possible optimization, to avoid wasted work.
bool checkedSiblings = false;
auto& options = getPassOptions();
while (1) {
if (auto* as = input->dynCast<RefAs>()) {
if (as->op == RefAsNonNull) {
// The problem with effect ordering that is described above is not an
// issue if traps are assumed to never happen anyhow.
if (!checkedSiblings && !options.trapsNeverHappen) {
// We need to see if a child with side effects exists after |input|.
// If there is such a child, it is a problem as mentioned above (it
// is fine for such a child to appear *before* |input|, as then we
// wouldn't be reordering effects).
bool seenInput = false;
for (auto* child : ChildIterator(parent)) {
if (child == input) {
seenInput = true;
} else if (seenInput) {
// TODO We could ignore trap effects here (since traps are ok to
// reorder) and also local effects (since a change to a var
// would not be noticeable, unlike say a global).
if (EffectAnalyzer(options, *getModule(), child)
.hasSideEffects()) {
Comment on lines +1512 to +1513
Copy link
Member

Choose a reason for hiding this comment

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

Would it be sufficient to check for control flow side effects here? Updating a local certainly won't matter, for instance. I guess updating a global might matter, though?

But I guess you'll say that side effects are rare enough that it doesn't really matter and we should do the simplest thing here 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, we could do a little better here. We can also ignore traps, since traps are ok to reorder. I added a TODO as it seems low priority, and would probably require some tinkering in effects.h.

return;
}
}
}
// If we got here, we've checked the siblings and found no problem.
checkedSiblings = true;
}
input = as->value;
continue;
}
Expand Down Expand Up @@ -1717,12 +1758,12 @@ struct OptimizeInstructions
}

void visitStructGet(StructGet* curr) {
skipNonNullCast(curr->ref);
skipNonNullCast(curr->ref, curr);
trapOnNull(curr, curr->ref);
}

void visitStructSet(StructSet* curr) {
skipNonNullCast(curr->ref);
skipNonNullCast(curr->ref, curr);
if (trapOnNull(curr, curr->ref)) {
return;
}
Expand Down Expand Up @@ -1878,12 +1919,12 @@ struct OptimizeInstructions
}

void visitArrayGet(ArrayGet* curr) {
skipNonNullCast(curr->ref);
skipNonNullCast(curr->ref, curr);
trapOnNull(curr, curr->ref);
}

void visitArraySet(ArraySet* curr) {
skipNonNullCast(curr->ref);
skipNonNullCast(curr->ref, curr);
if (trapOnNull(curr, curr->ref)) {
return;
}
Expand All @@ -1895,13 +1936,13 @@ struct OptimizeInstructions
}

void visitArrayLen(ArrayLen* curr) {
skipNonNullCast(curr->ref);
skipNonNullCast(curr->ref, curr);
trapOnNull(curr, curr->ref);
}

void visitArrayCopy(ArrayCopy* curr) {
skipNonNullCast(curr->destRef);
skipNonNullCast(curr->srcRef);
skipNonNullCast(curr->destRef, curr);
skipNonNullCast(curr->srcRef, curr);
trapOnNull(curr, curr->destRef) || trapOnNull(curr, curr->srcRef);
}

Expand Down Expand Up @@ -2155,7 +2196,7 @@ struct OptimizeInstructions
}

assert(curr->op == RefAsNonNull);
skipNonNullCast(curr->value);
skipNonNullCast(curr->value, curr);
if (!curr->value->type.isNullable()) {
replaceCurrent(curr->value);
return;
Expand Down
47 changes: 47 additions & 0 deletions test/lit/passes/optimize-instructions-gc-tnh.wast
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
;; NO_TNH: (type $void (func))
(type $void (func))

;; TNH: (import "a" "b" (func $import (result i32)))
;; NO_TNH: (import "a" "b" (func $import (result i32)))
(import "a" "b" (func $import (result i32)))

;; TNH: (func $ref.eq (type $eqref_eqref_=>_i32) (param $a eqref) (param $b eqref) (result i32)
;; TNH-NEXT: (ref.eq
;; TNH-NEXT: (local.get $a)
Expand Down Expand Up @@ -603,6 +607,49 @@
)
)

;; TNH: (func $null.cast-other.effects (type $ref?|$struct|_=>_none) (param $x (ref null $struct))
;; TNH-NEXT: (struct.set $struct 0
;; TNH-NEXT: (local.get $x)
;; TNH-NEXT: (call $import)
;; TNH-NEXT: )
;; TNH-NEXT: (struct.set $struct 0
;; TNH-NEXT: (local.get $x)
;; TNH-NEXT: (i32.const 10)
;; TNH-NEXT: )
;; TNH-NEXT: )
;; NO_TNH: (func $null.cast-other.effects (type $ref?|$struct|_=>_none) (param $x (ref null $struct))
;; NO_TNH-NEXT: (struct.set $struct 0
;; NO_TNH-NEXT: (ref.as_non_null
;; NO_TNH-NEXT: (local.get $x)
;; NO_TNH-NEXT: )
;; NO_TNH-NEXT: (call $import)
;; NO_TNH-NEXT: )
;; NO_TNH-NEXT: (struct.set $struct 0
;; NO_TNH-NEXT: (local.get $x)
;; NO_TNH-NEXT: (i32.const 10)
;; NO_TNH-NEXT: )
;; NO_TNH-NEXT: )
(func $null.cast-other.effects (param $x (ref null $struct))
(struct.set $struct 0
;; We cannot remove this ref.as_non_null, even though the struct.set will
;; trap if the ref is null, because that would move the trap from before
;; the call to the import to be after it. But in TNH we can assume it does
;; not trap, and remove it.
(ref.as_non_null
(local.get $x)
)
(call $import)
)
(struct.set $struct 0
;; This one can be removed even without TNH, as there are no effects after
;; it.
(ref.as_non_null
(local.get $x)
)
(i32.const 10)
)
)

;; Helper functions.

;; TNH: (func $get-i32 (type $none_=>_i32) (result i32)
Expand Down