Skip to content

Conversation

@kripken
Copy link
Member

@kripken kripken commented Jul 3, 2024

Previously the pass would monomorphize a call when we were sending more
refined types than the target expects. This generalizes the pass to also consider
the case where we send a constant in a parameter.

To achieve that, this refactors the pass to explicitly define the "call context",
which is the code around the call (inputs and outputs) that may end up leading
to optimization opportunities when combined with the target function. Also
add comments about the overall design + roadmap.

The existing test is mostly unmodified, and the diff there is smaller when
ignoring whitespace. We do "regress" those tests by adding more local.set
operations, as in the refactoring that makes things a lot simpler, that is, to
handle the general case of an operand having either a refined type or be a
constant, we copy it inside the function, which works either way. This
"regression" is only in the testing version of the pass (the normal version
runs optimizations, which would remove that extra code).

This also enables the pass when GC is disabled. Previously we only handled
refined types, so only GC could benefit. Add a test for MVP content
specifically to show we operate there as well.

@kripken kripken requested a review from tlively July 3, 2024 17:22
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.

Comments on the code, will look at tests in the morning.

}

std::unique_ptr<Function>
copyFunctionWithoutAdd(Function* func,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe it would be better to rename copyFunction to copyAndAddFunction as a preliminary change so this can just be copyFunction? Alternatively, we could keep only the behavior of copying without adding; it's not too burdensome for callers to call module.addFunction themselves.

Copy link
Member Author

Choose a reason for hiding this comment

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

A downside to copyAndAddFunction (or to keeping the name but dropping the add) is that then it would not be consistent with copyGlobal, copyTable, etc. which all do the natural thing and add.

It is the not-adding that is the slightly odd thing here, but this code really benefits from it, and it seems not that big a burden to have a public API for it I think.

Copy link
Member

Choose a reason for hiding this comment

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

We definitely want consistency. IMO it would be cleanest if none of the copyXXX functions did the add because then they would all be pure, but no need to block this PR on that.

Comment on lines +60 to +65
// The empirical approach significantly reduces the need for heuristics. For
// example, rather than have a heuristic for "see if a constant parameter flows
// into a conditional branch," we simply run the optimizer and let it optimize
// that case. All other cases handled by the optimizer work as well, without
// needing to specify them as heuristics, so this gets smarter as the optimizer
// does.
Copy link
Member

Choose a reason for hiding this comment

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

What is the performance overhead of this approach? It would be interesting to know what percent of the work turns out to be not beneficial and thrown away.

Copy link
Member Author

Choose a reason for hiding this comment

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

Most of the work is thrown out, I'd bet, but how much depends on heuristics not yet implemented, like we could ignore functions over a certain size, etc.

Overall I think it is fine to waste work here: that's ok if it lets us find optimization opportunities not possible any other way (which I hope is the case 😄 but have not yet proven, though I do have particular use cases that I am confident about).

Fundamentally, rigid heuristics may save work but miss opportunities, while try-it-and-see can find more in return for throwing some work away.

}
}

return dropped == other.dropped;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe check this before the operands because it is so cheap?

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 idea, done.

bool canBeMovedIntoContext(Expression* curr) {
// Constant numbers, funcs, strings, etc. can all be copied, so it is ok to
// add them to the context.
return Properties::isSingleConstantExpression(curr);
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this is not true for constant global.get, but that might be good to include as well.

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, yeah, this could capture anything "copyable" really. I added a TODO.

Comment on lines 269 to 270
[[maybe_unused]] std::ostream& operator<<(std::ostream& o,
wasm::CallContext& context) {
Copy link
Member

Choose a reason for hiding this comment

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

If you name this dump() or something, it will be callable from a debugger, and therefore even more useful.

Copy link
Member Author

Choose a reason for hiding this comment

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

I never use a debugger myself, so I would never have thought of that 😄 Done.

Comment on lines 404 to 406
// TODO: check for either a size decrease (always good) or a significant
// speed increase (as a tiny one, in a huge function, can lead to
// wasteful duplicated code)
Copy link
Member

Choose a reason for hiding this comment

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

Why is code size decreasing always good? Do we add the monomorphized function only when we can entirely replace the original function?

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 catch, yeah, this TODO makes more sense for the case we can entirely replace, which is not the case here. I revised it.

}

// The main body of the function is simply copied from the original.
auto* newBody = ExpressionManipulator::copy(func->body, wasm);
Copy link
Member

Choose a reason for hiding this comment

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

Haven't we already made a copy of the body when we copied the function? Why do we need another copy?

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 catch, this was a leftover from an older approach. Fixed.

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.

LGTM!

@kripken kripken merged commit 5a1daf7 into WebAssembly:main Jul 11, 2024
@kripken kripken deleted the mono.moar branch July 11, 2024 17:31
@gkdn gkdn mentioned this pull request Aug 31, 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.

2 participants