Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make br_if with a value's type match the spec #6390

Closed
wants to merge 50 commits into from

Conversation

kripken
Copy link
Member

@kripken kripken commented Mar 8, 2024

Previously br_if with a value had the same type as the value, but the spec
says it should be the type of the block it targets, which might be less refined.
Context:

WebAssembly/gc#516 (comment)

This is unfortunate for Binaryen as the value is right there in our IR, while
looking up the block's type is more effort. I measure a 1.5% slowdown in
building a J2Wasm testcase with this. There do not seem to be downsides
to the output, but that is just because the value output of br_if is practically
never used outside of fuzz testcases and exploits.

Summary of the necessary changes for this:

  • Adjust validator to allow the spec form only.
  • Add optional Type param to the C API to create a Break.
  • ReFinalize may do another pass if it ends up updating br_ifs - we can no
    longer just do a simple forward pass here, as block changes go "backwards"
    to br_ifs. In practice I think that should be rare (on J2Wasm I see only a
    3 times where an extra iteration is needed when running -O3).
  • Update br_if types in the few passes that modify br_if values or add
    values or conditions to brs, including Heap2Local, RemoveUnusedBrs,
    SimplifyLocals.
  • RemoveUnusedBrs also must be careful not to un-refine block types. It
    used to do that, and they generally ended up refined later anyhow, but now
    this would be a danger (they would unrefine the br_if, which might break).
  • The parsers must provide br_if types during construction.
  • Add Type.containsRef() helper.

@kripken kripken requested a review from tlively March 8, 2024 23:39
Copy link
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

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

I hadn't thought about the fact that calculating precise types requires a fixed point computation after this fix. Maybe we should press a little harder on this issue after all.

// After that walk we may have br_ifs in need of refinalization. Update them
// and refinalize again, as they may enable further improvements. This is in
// theory very slow, but in practice one or two cycles suffices and we can't
// try to be frugal here as must propagate all the possible improvements, or
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// try to be frugal here as must propagate all the possible improvements, or
// try to be frugal here as we must propagate all the possible improvements, or

if (auto* ret = curr->dynCast<Return>()) {
value = ret->value;
} else {
value = curr->cast<Break>()->value;
Copy link
Member

Choose a reason for hiding this comment

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

This can never be a Switch or any other branching instruction?

// to GC). This also calls finalize as needed, so that after calling it the
// br_if type is fully updated.
using BlockTypeMap = std::unordered_map<Name, Type>;
static void updateBrIfType(Break* br, const BlockTypeMap& blockTypeMap) {
Copy link
Member

Choose a reason for hiding this comment

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

Is this to avoid having to do a full ReFinalize to fix the br_if types? If so, is it really worth the extra complexity?


// Note types of blocks. This parallels the logic in the parent class,
// but we do not want to just reuse the data structure there: things may
// have changed since then.
Copy link
Member

Choose a reason for hiding this comment

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

Changed since when?

Comment on lines +828 to +837
if (isTuple()) {
for (auto t : *this) {
if (t.isRef()) {
return true;
}
}
return false;
}
return isRef();
}
Copy link
Member

Choose a reason for hiding this comment

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

You can iterate through non-tuple types as well, and the iterator will just dereference to the type itself.

Suggested change
if (isTuple()) {
for (auto t : *this) {
if (t.isRef()) {
return true;
}
}
return false;
}
return isRef();
}
for (auto t : *this) {
if (t.isRef()) {
return true;
}
}
return false;
}

Comment on lines +2606 to +2609
// There is no block by that name, so this must target the function scope.
// We could also validate that it is in fact the function scope, but the
// main validator does that anyhow; all we need here is to generate valid
// IR if it is valid.
Copy link
Member

Choose a reason for hiding this comment

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

Although it has been suggested as an extension to the text format, there is not currently a way to use a symbolic name to have a branch target the function scope, so I don't think the old parser needs to (or should be able to) support this case.

@@ -781,6 +797,11 @@ void FunctionValidator::visitLoop(Loop* curr) {
"breaks to a loop cannot pass a value");
}
breakTypes.erase(iter);

// Validate there are no br_ifs with values targeting us.
shouldBeTrue(!labelBrIfs.count(curr->name),
Copy link
Member

Choose a reason for hiding this comment

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

Is this not covered by the breakTypes check above?

@tlively
Copy link
Member

tlively commented Mar 9, 2024

I also just realized that this refinalization algorithm does not find the smallest fixed point of the types (i.e. the most refined possible block and br_if types).

Consider this program:

(type $A (sub (struct)))
(type $B (sub $A (struct)))

(func (result (ref $A))
 (block $a (result (ref $A))
  (br_if $a
   (struct.new $B)
   (i32.const 0)
  )
 ) 
)

If I'm not mistaken, ReFinalize will not be able to improve the type of the block because it calculates the type of the block as the LUB of the sent values of the br_if and the fallthrough value, which is (ref $A). It will also never improve the type of the br_if.

The fix would be to do the first ReFinalization walk with the old behavior of setting the br_if type equal to its value's type, then doing subsequent iterations with the new behavior to relax the types up to the smallest fixed point.

@kripken
Copy link
Member Author

kripken commented Mar 9, 2024

The fix would be to do the first ReFinalization walk with the old behavior of setting the br_if type equal to its value's type, then doing subsequent iterations with the new behavior to relax the types up to the smallest fixed point.

Good point about this not necessarily reaching the optimal point. And, interesting idea to solve it.

Thinking about that, perhaps another option is to first walk with the old behavior, and then to add casts as necessary afterwards, without any further iterations? That is, any br_if that we must force to be a less-precise type than it "wants" to be would be cast so that it does flow out the type it "wants." Adding casts is not ideal, but that situation is quite rare.

@tlively
Copy link
Member

tlively commented Mar 9, 2024

Oh yeah, adding casts seems fine, too. It gets back all of the type precision at the cost of having to execute the casts at runtime. How much do you think that would simplify this PR beyond ReFinalize itself?

Since following the spec rules has more downsides than I had anticipated, I also started pushing more to get them changed: WebAssembly/gc#516 (comment).

@kripken
Copy link
Member Author

kripken commented Mar 9, 2024

How much do you think that would simplify this PR beyond ReFinalize itself?

Hmm, good question. I think adding the casts would remove a little bit of complexity (while not adding much extra tracking logic). But that would still leave 99% of the complexity in this PR. Good idea to discuss spec options.

@tlively
Copy link
Member

tlively commented Mar 9, 2024

If we are able to change the spec, then we'll have to fix our handling of local.tee instead. local.tee would no longer be exactly equivalent to a local.set + local.get. IIRC, you did some experiments with that a while ago and decided that wouldn't be a big problem, right?

@kripken
Copy link
Member Author

kripken commented Mar 9, 2024

Yes, updating local.tee would require some work but we thought it was not a big burden: #3704 (it's possible changes since then make things worse but I doubt it).

Reading that and WebAssembly/gc#201 again, it's hard not to see how we seem to keep returning to this topic again and again... 😄 🔁

@kripken
Copy link
Member Author

kripken commented May 15, 2024

Closing in favor of #6510

@kripken kripken closed this May 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants