Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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
5 changes: 4 additions & 1 deletion src/ir/localize.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
namespace wasm {

// Make an expression available in a local. If already in one, just
// use that local, otherwise use a new local
// use that local, otherwise use a new local.
//
// Note that if the local is reused, this assumes it is not modified in between
// the set and the get, which the caller must ensure.

struct Localizer {
Index index;
Expand Down
69 changes: 69 additions & 0 deletions src/ir/reorderer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2021 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#ifndef wasm_ir_reorderer_h
#define wasm_ir_reorderer_h

#include <ir/effects.h>
#include <wasm-builder.h>

namespace wasm {

//
// Reorder two concrete expressions, using a local if necessary. Given
//
// (first, second)
//
// we generate something so that it is valid to write
//
// (Reorderer.second, Reorderer.first)
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.

How about calling the new first first and the new second second? Because it is within Reorderer, I feel like it is less confusing for Reorderer's first should be the reordered first. To me it is harder to read the code assuming that this first should go second in the result and vice versa.

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.

Interesting, it's the opposite for me 😄

I think about it as "maybe the Reorderer will do nothing, and so Reorderer.second = second, which means the user wants to put that first when they reorder".

How about (Reorderer.reorderedFirst, Reorderer.reorderedSecond)? But that is still ambiguous...

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, my preference is in the order of first/second > reorderedFirst/reorderedSecond > second/first(the status quo), but if you feel this is more confusing, it is possible that this is just my intuition. I don't feel strongly, so let's leave it then.

//
// (note the reversal of the order there). Reorderer.first/second will contain
// either the original two expressions (first in first, second in second), or
// ones with local usage to work around interactions between them.
//

struct Reorderer {
Expression* first;
Expression* second;

Reorderer(Expression* firstInput,
Expression* secondInput,
Function* func,
Module* wasm,
const PassOptions& passOptions) {
assert(firstInput->type.isConcrete());
assert(secondInput->type.isConcrete());

if (EffectAnalyzer::canReorder(
passOptions, wasm->features, firstInput, secondInput)) {
first = firstInput;
second = secondInput;
return;
}

auto type = firstInput->type;
auto index = Builder::addVar(func, type);
Builder builder(*wasm);
second = builder.makeSequence(builder.makeLocalSet(index, firstInput),
secondInput);
first = builder.makeLocalGet(index, type);
}
};

} // namespace wasm

#endif // wasm_ir_reorderer_h
119 changes: 91 additions & 28 deletions src/passes/OptimizeInstructions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <ir/manipulation.h>
#include <ir/match.h>
#include <ir/properties.h>
#include <ir/reorderer.h>
#include <ir/type-updating.h>
#include <ir/utils.h>
#include <pass.h>
Expand Down Expand Up @@ -212,20 +213,33 @@ struct OptimizeInstructions

bool fastMath;

bool refinalize = false;

void doWalkFunction(Function* func) {
fastMath = getPassOptions().fastMath;
// first, scan locals

// First, scan locals.
{
LocalScanner scanner(localInfo, getPassOptions());
scanner.setModule(getModule());
scanner.walkFunction(func);
}
// main walk

// Main walk.
super::doWalkFunction(func);

// If we need to update parent types, do so.
if (refinalize) {
ReFinalize().walkFunctionInModule(func, getModule());
}

// Final optimizations.
{
FinalOptimizer optimizer(getPassOptions());
optimizer.walkFunction(func);
}

TypeUpdating::handleNonDefaultableLocals(func, *getModule());
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.

Would be helpful to add a comment that Reorderer can create locals so we need this

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.

Good point, adding a comment.

}

// Set to true when one of the visitors makes a change (either replacing the
Expand Down Expand Up @@ -1263,41 +1277,78 @@ struct OptimizeInstructions
skipNonNullCast(curr->srcRef);
}

bool canBeCastTo(HeapType a, HeapType b) {
return HeapType::isSubType(a, b) || HeapType::isSubType(b, a);
}

void visitRefCast(RefCast* curr) {
if (curr->type == Type::unreachable) {
return;
}

Builder builder(*getModule());
auto passOptions = getPassOptions();

auto fallthrough = Properties::getFallthrough(
curr->ref, getPassOptions(), getModule()->features);

// If the value is a null, it will just flow through, and we do not need the
// cast. However, if that would change the type, then things are less
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.

If this expression is null and changes type from such as a struct to an array, would that possibly make the parent or grandparent expressions of this not validate? We fix nullability but what if the heaptype itself changes? Can Refinailize solve 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.

The code here is actually careful about the heap type, I'll improve the comment though.

Basically, we use the heap type of the RTT to create the null - that is the type on the outside.

// simple: if the original type was non-nullable, replacing it with a null
// would change the type, which can happen in e.g.
// (ref.cast (ref.as_non_null (.. (ref.null)
if (fallthrough->is<RefNull>()) {
Expression* rep =
builder.makeBlock({builder.makeDrop(curr->ref),
builder.makeDrop(curr->rtt),
builder.makeRefNull(curr->rtt->type.getHeapType())});
if (curr->ref->type.isNonNullable()) {
// Avoid a type change by forcing to be non-nullable. In practice, this
// would have trapped before we get here, so this is just for
Comment on lines +1311 to +1312
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 would this have trapped before we get here?

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 receive a null value from a place that has a non-nullable type. Somehow the null falls through there, which I think is only possible using ref.as_non_null which will trap on a null.

// validation.
rep = builder.makeRefAs(RefAsNonNull, rep);
}
replaceCurrent(rep);
return;
// TODO: The optimal ordering of this and the other ref.as_non_null stuff
// later down in this functions is unclear and may be worth looking
// into.
}

// For the cast to be able to succeed, the value being cast must be a
// subtype of the desired type, as RTT subtyping is a subset of static
// subtyping. For example, trying to cast an array to a struct would be
// incompatible.
if (!canBeCastTo(curr->ref->type.getHeapType(),
curr->rtt->type.getHeapType())) {
// This cast cannot succeed. It will either trap if the input is not a
// null, or it will return null if it is (and we have already handled the
// case of null before).
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 sounds like this can't be null because we've already handled the case, but we've only handed it is ref.null, and this can still be null at runtime (as the comment in line 1335-1336 says). I think this comment is a little confusing.

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.

Good point, yeah, this is unclear. I'll rephrase.

if (fallthrough->type.isNonNullable()) {
// Our type will now be unreachable; update the parents.
refinalize = true;
replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref),
builder.makeDrop(curr->rtt),
builder.makeUnreachable()}));
return;
}
// Otherwise, we are not sure what it is, and need to wait for runtime to
// see if it is a null or not.
}

if (passOptions.ignoreImplicitTraps || passOptions.trapsNeverHappen) {
// A ref.cast traps when the RTTs do not line up, which can be of one of
// two sorts of issues:
// 1. The value being cast is not even a subtype of the cast type. In
// that case the RTTs trivially cannot indicate subtyping, because
// RTT subtyping is a subset of static subtyping. For example, maybe
// we are trying to cast a {i32} struct to an [f64] array.
// 2. The value is a subtype of the cast type, but the RTTs still do not
// fit. That indicates a difference between RTT subtyping and static
// subtyping. That is, the type may be right but the chain of rtt.subs
// is not.
// If we ignore a possible trap then we would like to assume that neither
// of those two situations can happen. However, we still cannot do
// anything if point 1 is a problem, that is, if the value is not a
// subtype of the cast type, as we can't remove the cast in that case -
// the wasm would not validate. But if the type *is* a subtype, then we
// can ignore a possible trap on 2 and remove it.
//
// We also do not do this if the arguments cannot be reordered. If we
// can't do that then we need to add a drop, at minimum (which may still
// be worthwhile, but depends on other optimizations kicking in, so it's
// not clearly worthwhile).
// Aside from the issue of type incompatibility as mentioned above, the
// cast can trap if the types *are* compatible but it happens to be the
// case at runtime that the value is not of the desired subtype. If we
// do not consider such traps possible, we can ignore that. Note, though,
// that we cannot do this if we cannot replace the current type with the
// reference's type.
Comment on lines +1345 to +1350
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.

Are traps possible even if ref's heap type is a subtype of rtt's heap type?

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, this is an unfortunate property of rtts. E.g. (rtt.canon $x) != (rtt.sub $x (rtt.canon $x)).

This is bad for optimization, so we are hoping to prototype a more static approach soon, that would be as you describe basically.

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.

(rtt.canon $x) != (rtt.sub $x (rtt.canon $x))

😨

if (HeapType::isSubType(curr->ref->type.getHeapType(),
curr->rtt->type.getHeapType()) &&
canReorder(curr->ref, curr->rtt)) {
Builder builder(*getModule());
replaceCurrent(
builder.makeSequence(builder.makeDrop(curr->rtt), curr->ref));
curr->rtt->type.getHeapType())) {
Reorderer reorderer(
curr->ref, curr->rtt, getFunction(), getModule(), passOptions);
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.

Do we not need to check if they can be reordered effect-wise?

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.

That is what the Reorderer does - it potentially adds more code to handle that problem (it adds a local and saves the value here, keeping their order identical).

replaceCurrent(builder.makeSequence(builder.makeDrop(reorderer.second),
reorderer.first));
return;
}
}
Expand Down Expand Up @@ -1358,6 +1409,18 @@ struct OptimizeInstructions
}
}

void visitRefTest(RefTest* curr) {
// See above in RefCast.
if (!canBeCastTo(curr->ref->type.getHeapType(),
curr->rtt->type.getHeapType())) {
// This test cannot succeed, and will definitely return 0.
Builder builder(*getModule());
replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref),
builder.makeDrop(curr->rtt),
builder.makeConst(int32_t(0))}));
}
}

void visitRefIs(RefIs* curr) {
if (curr->type == Type::unreachable) {
return;
Expand Down
6 changes: 1 addition & 5 deletions test/lit/passes/inlining-optimizing.wast
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
(module
;; CHECK: (type $none_=>_none (func))
(type $none_=>_none (func))
;; CHECK: (type $none_=>_i32 (func (result i32)))
(type $none_=>_i32 (func (result i32)))
;; CHECK: (func $0
;; CHECK-NEXT: (nop)
Expand All @@ -15,10 +14,7 @@
;; CHECK: (func $1
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (call_ref
;; CHECK-NEXT: (ref.cast
;; CHECK-NEXT: (ref.func $0)
;; CHECK-NEXT: (rtt.canon $none_=>_i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
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.

Do we need to change something in the comment below?

;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
Expand Down
11 changes: 8 additions & 3 deletions test/lit/passes/optimize-instructions-call_ref.wast
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,14 @@

;; CHECK: (func $fallthrough-bad-type (result i32)
;; CHECK-NEXT: (call_ref
;; CHECK-NEXT: (ref.cast
;; CHECK-NEXT: (ref.func $return-nothing)
;; CHECK-NEXT: (rtt.canon $none_=>_i32)
;; CHECK-NEXT: (block
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (ref.func $return-nothing)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (rtt.canon $none_=>_i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
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.

Here too; does the comment need changing?

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.

Sounds good, thanks, I'm updating these comments too.

;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
Expand Down
Loading