Skip to content

Simplify BinaryenIRWriter #3110

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

Merged
merged 4 commits into from
Sep 11, 2020

Conversation

tlively
Copy link
Member

@tlively tlively commented Sep 10, 2020

BinaryenIRWriter was previously inconsistent about whether or not it
emitted an instruction if that instruction was not reachable.
Instructions that produced values were not emitted if they were
unreachable, but instructions that did not produce values were always
emitted. Additionally, blocks continued to emit their children even
after emitting an unreachable child.

Since it was not possible to tell whether an unreachable instruction's
parent would be emitted, BinaryenIRWriter had to be very defensive and
emit many extra unreachable instructions around unreachable code to
avoid type errors.

This PR unifies the logic for emitting all non-control flow
instructions and changes the behavior of BinaryenIRWriter so that it
never emits instructions that cannot be reached due to having
unreachable children. This means that extra unreachable instructions
now only need to be emitted after unreachable control flow
constructs. BinaryenIRWriter now also stops emitting instructions
inside blocks after the first unreachable instruction as an extra
optimization.

This change will also simplify Poppy IR stackification (see #3059) by
guaranteeing that instructions with unreachable children will not be
emitted into the stackifier. This makes satisfying the Poppy IR rule
against unreachable Pops trivial, whereas previously satisfying this
rule would have required about about 700 additional lines of code to
recompute the types of all unreachable children for any instruction.

BinaryenIRWriter was previously inconsistent about whether or not it
emitted an instruction if that instruction was not reachable.
Instructions that produced values were not emitted if they were
unreachable, but instructions that did not produce values were always
emitted. Additionally, blocks continued to emit their children even
after emitting an unreachable child.

Since it was not possible to tell whether an unreachable instruction's
parent would be emitted, BinaryenIRWriter had to be very defensive and
emit many extra `unreachable` instructions around unreachable code to
avoid type errors.

This PR unifies the logic for emitting all non-control flow
instructions and changes the behavior of BinaryenIRWriter so that it
never emits instructions that cannot be reached due to having
unreachable children. This means that extra `unreachable` instructions
now only need to be emitted after unreachable control flow
constructs. BinaryenIRWriter now also stops emitting instructions
inside blocks after the first unreachable instruction as an extra
optimization.

This change will also simplify Poppy IR stackification (see WebAssembly#3059) by
guaranteeing that instructions with unreachable children will not be
emitted into the stackifier. This makes satisfying the Poppy IR rule
against unreachable Pops trivial, whereas previously satisfying this
rule would have required about about 700 additional lines of code to
recompute the types of all unreachable children for any instruction.
@tlively tlively requested review from kripken and aheejin September 10, 2020 09:00
@tlively
Copy link
Member Author

tlively commented Sep 10, 2020

So far this has done 7000 iterations on the fuzzer, but I will leave it running over night.

@tlively
Copy link
Member Author

tlively commented Sep 10, 2020

Alright, I just stopped the fuzzer at 67892 iterations, so I'd say this is stable.

Copy link
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

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

Very nice!

Two things though:

  1. Does this have a performance impact? Using an Iterator instead of a direct walk may be slower. Worth measuring.
  2. Might be good to run the emscripten test suite before landing (maybe at least other, wasm0, wasm2).

if (Properties::isControlFlowStructure(curr)) {
// If conditions are the only stack children of control flow structures
if (auto* if_ = curr->dynCast<If>()) {
self->pushTask(SubType::scan, &if_->condition);
Copy link
Member

Choose a reason for hiding this comment

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

we use iff for this type of thing in more places

// similar to in visitBlock, here we could skip emitting the block itself,
// but must still end the 'block' (the contents, really) with an unreachable
emitUnreachable();
if (child->type == Type::unreachable) {
Copy link
Member

Choose a reason for hiding this comment

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

maybe a comment here about why we do this and why it is valid? specifically that this is enough because an unreachable block must have an unreachable child.

class ChildIterator {
// ChildIterator - Iterates over all children
//
// StackChildIterator - Iterates over all children that produce values used by
Copy link
Member

Choose a reason for hiding this comment

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

"Stack" seems not clear enough to me. If this is just for Poppy IR then "Poppy" (or "Stacky" if we decided on that name?). But that doesn't seem clear enough either. How about PoppedChildIterator - indicating that these are children that are popped?

Copy link
Member Author

Choose a reason for hiding this comment

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

This isn't particular to Poppy IR, and I'm concerned that having "Popped" in the name might give the wrong impression. Maybe ValueChildIterator?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, maybe "value child" vs "structural child" or such is good terminology. +1 for ValueChildIterator

@tlively
Copy link
Member Author

tlively commented Sep 10, 2020

I checked the performance of roundtrip on a different on a 1.3MB binary. Times are averages over 5 runs.

  • Before this PR: 0.122s
  • After this PR: 0.151s
  • This PR with SmallVector: 0.135s

So we are taking a 10% hit on our baseline parse + emit performance. This is not great, but I think it is worth it for the simplicity wins. I don't feel strongly about that, though, and I would be willing to make the same behavioral change without unifying all the logic. Let me know what you think.

Also, the emscripten tests passed.

@kripken
Copy link
Member

kripken commented Sep 10, 2020

(What's the SmallVector change? In the Iterator?)

I think 10% might be acceptable here. We are moving to make unoptimized builds not run binaryen at all, and in optimized builds binary writing is pretty small compared to optimization work. But might be worth a TODO to look into optimizing this more, as in theory the iterator could be as fast as a walk.

@tlively
Copy link
Member Author

tlively commented Sep 10, 2020

Yeah, sorry, I should have explained the SmallVector change. That's with a SmallVector<Expression*, 4> instead of a std::vector<Expression*> in the iterator.

What we really need is a kind of Walker where tasks can return a value to make walk return without emptying the task stack and add a continueWalk method to pick up where the previous walk left off. The real trick would be to integrate that nicely into the existing walker framework without making anything more expensive.

@tlively tlively merged commit cd6f0d9 into WebAssembly:master Sep 11, 2020
@tlively tlively deleted the binaryen-ir-writer-unreachables branch September 11, 2020 00:49
@aheejin
Copy link
Member

aheejin commented Sep 11, 2020

Late to the review, but it is really a nice simplifying change!

aheejin added a commit to aheejin/binaryen that referenced this pull request Jan 11, 2021
This test seems to be added in WebAssembly#2266 to test custom unreachable
generation in `BinaryenIRWriter`, but given that the `fromBinary` files
only contain a single `unreaachable` for the whole function, I don't
think this tests serves a lot of purpose. Also the custom unreachable
generation logic in WebAssembly#2266 was largely replaced in WebAssembly#3110.
aheejin added a commit to aheejin/binaryen that referenced this pull request Jan 11, 2021
This test seems to be added in WebAssembly#2266 to test custom unreachable
generation in `BinaryenIRWriter`, but given that the `fromBinary` files
only contain a single `unreaachable` for the whole function, I don't
think this test serves a lot of purpose. Also the custom unreachable
generation logic in WebAssembly#2266 was largely replaced in WebAssembly#3110.
aheejin added a commit to aheejin/binaryen that referenced this pull request Jan 11, 2021
This test seems to be added in WebAssembly#2266 to test custom unreachable
generation in `BinaryenIRWriter`, but given that the `fromBinary` files
only contain a single `unreaachable` for the whole function, I don't
think this test serves a lot of purpose. Also the custom unreachable
generation logic in WebAssembly#2266 was largely replaced in WebAssembly#3110.
aheejin added a commit that referenced this pull request Jan 12, 2021
This test seems to be added in #2266 to test custom unreachable
generation in `BinaryenIRWriter`, but given that the `fromBinary` files
only contain a single `unreachable` for the whole function, I don't
think this test serves a lot of purpose. Also the custom unreachable
generation logic in #2266 was largely replaced in #3110.
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.

3 participants