Skip to content

[Wasm GC] Optimize RefIs and RefAs when the type lets us - #3725

Merged
kripken merged 33 commits into
mainfrom
optrefasis2
Mar 30, 2021
Merged

kripken merged 33 commits into
mainfrom
optrefasis2

Conversation

@kripken

@kripken kripken commented Mar 24, 2021

Copy link
Copy Markdown
Member

This is similar to the optimization of BrOn in #3719 and #3724. When the
type tells us the kind of input we have, we can tell at compile time what
result we'll get, like ref.is_func of something with type (ref func) will
always return 1, etc.

Those previous PRs optimized things that look obvious in terms of what
Binaryen should be doing, but this PR is a little less clear to me (especially
after @aheejin 's comments in #3724 that made me realize this is less
trivial than it seems), so cc'ing some VM people for thoughts:

@jakobkummerow @manoskouk @lars-t-hansen @eqrion

The main open question is the second TODO in the code, which is
basically: Is ref.is_func as fast as ref.is_null? If the latter is a null
check then maybe not. And what about ref.is_func vs the combination
of ref.is_null + i32.eqz?

@kripken
kripken requested review from aheejin and tlively March 29, 2021 19:46
Comment on lines +1016 to +1017
// since RefIsNull is a single byte (the others are 2), while adding a Const
// of 0 would be two bytes. Other factors are that we can remove the input

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.

Does this include drop in the calculation too? For example, in RefIsNull case, we save one byte by removing ref.is_null, add one byte by drop, and add two more bytes for i32.const x, so isn't that net +2 bytes gain? (+1 byte gain for other ref.is_*** instructions)

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.

I should have mentioned the drop too, yes. In most cases the input value has no side effects so both it and the drop for it can be removed.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
// This is the wrong kind, so it will trap. The binaryen optimizer does
// not differentiate traps, so we can perform a replacement here. We
// replace 2 bytes of ref.as_* with one byte of unreachable and one of a
// drop, and the drop can be optimized out later, so this saves both work

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.

and the drop can be optimized out later

Isn't this the case only when the value does not have a side effect?

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.

Correct, I'll fix the comment.

@aheejin aheejin left a comment

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.

By the way did the VM people respond to your questions on the efficiency of the instructions?

;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $unneeded_is

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.

Existing tests in this file uses 2-space indent, so better be consistent? (I know 1-space and 2-space indents are mixed in many of our tests, but I guess it'd be good to be consistent for new tests)

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, I'll fix the whitespace.

;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $unneeded_is_bad_kinds
(param $func (ref null func))

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.

Suggested change
(param $func (ref null func))
(param $func (ref func))

Make one of the params non-nullable to test both nullable and non-nullable cases?

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 tested lower down, we do ref.as_non_null on the nullable params to get non-nullable inputs. I'll add a comment to make it more obvious to the eye.

;; CHECK-NEXT: )
(func $unneeded_as_bad_kinds
(param $func (ref null func))
(param $data (ref null data))

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.

Suggested change
(param $data (ref null data))
(param $data (ref data))

Make one of the params non-nullable to test both nullable and non-nullable cases?

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 catch. I'll add three more testcases below to check those, and a comment.

@askeksa-google

Copy link
Copy Markdown

In TurboFan at least, any outermost i32.eqz in the condition of an if or br_if are folded into the branch condition, so they are essentially free.

@kripken

kripken commented Mar 30, 2021

Copy link
Copy Markdown
Member Author

Thanks @askeksa-google ! Makes sense. And binaryen should remove an i32.eqz in those cases, so any left over would need to be actually executed. What's not obvious is how common that is, I guess.

@aheejin That's the only response so far from people familiar with VMs. People are busy I guess...

If we don't get more detailed responses I still think this is a good direction for now. But we should benchmark and evaluate these more later.

@kripken
kripken merged commit 38c1193 into main Mar 30, 2021
@kripken
kripken deleted the optrefasis2 branch March 30, 2021 19:32
martianboy pushed a commit to martianboy/binaryen that referenced this pull request Mar 31, 2021
…#3725)

This is similar to the optimization of BrOn in WebAssembly#3719 and WebAssembly#3724. When the
type tells us the kind of input we have, we can tell at compile time what
result we'll get, like ref.is_func of something with type (ref func) will
always return 1, etc.

There are some code size and perf tradeoffs that should be looked into
more and are marked as TODOs.
@jakobkummerow

Copy link
Copy Markdown
Contributor

Is ref.is_func as fast as ref.is_null?

I would assume that ref.is_null is faster, though as always it's difficult to say by how much.

In our current implementation, a null check is a pointer comparison against a known constant (which in turn needs to be loaded or materialized, the cost of which depends on how long it's been since the last null check). A func check involves two chained loads (object -> shape descriptor -> type). In fact, our current implementation of is_func initially performs an is_null check as a fast path; however that's technically not necessary and we should probably measure at some point whether it makes more sense to keep or to drop that fast path.

@kripken

kripken commented Apr 7, 2021

Copy link
Copy Markdown
Member Author

I see, thanks @jakobkummerow - very good to know the details here.

Sounds like this PR is in the right direction then. And we can evaluate more in detail as we get benchmarks.

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.

4 participants