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

[compiler] Promote temporaries when necessary to prevent codegen reordering over side-effectful operations #30554

Merged
merged 5 commits into from
Aug 12, 2024

Conversation

mvitousek
Copy link
Contributor

@mvitousek mvitousek commented Jul 31, 2024

Stack from ghstack (oldest at bottom):

This PR addresses the issue recorded here, where codegen reorders instructions by moving a temporary across a promoted temporary (or an instruction whose lvalue has been pruned) in an unsafe way.

We fix this by adding a new sub-pass to PromoteUsedTemporaries which looks for cases where the definition and the use of an unpromoted temporary, whose value could potentially be mutated, have an instruction in between them that 1. can cause mutation and 2. will be emitted as a separate statement. For our purposes, 1. means function calls and stores, and 2. means either that we've pruned the lvalue or that we've promoted the identifier (or technically that the identifier was named in the first place, but I don't think we could be reodering across it in the first place for this case). If we find such a temporary with an interposing mutative instruction, we'll promote it to a temporary as well.

…dering over side-effectful operations

[ghstack-poisoned]
Copy link

vercel bot commented Jul 31, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 12, 2024 7:43pm

mvitousek added a commit that referenced this pull request Jul 31, 2024
…dering over side-effectful operations

ghstack-source-id: 3f0e3b6c9d66c592c0f95806bf95410f22b10e68
Pull Request resolved: #30554
…odegen reordering over side-effectful operations"

[ghstack-poisoned]
mvitousek added a commit that referenced this pull request Aug 1, 2024
…dering over side-effectful operations

ghstack-source-id: f9a0f2337bae11781e4e12448784816b49ebd652
Pull Request resolved: #30554
@mvitousek mvitousek marked this pull request as ready for review August 1, 2024 16:27
Copy link
Contributor

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

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

This overall makes sense, just a few questions/comments. I'd love eyes on it from @mofeiZ as well.

/*
* Unpromoted temporaries will be emitted at their use sites rather than as separate
* declarations. However, this causes errors if an interposing temporary has been
* promoted, or if an interposing instruction has had its lvalues
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like text got cut off here?

Comment on lines 270 to 273
state.set(instruction.lvalue.identifier.id, [
instruction.lvalue.identifier,
false,
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like you could avoid creating preEntries (which will addd up since we create it for every such instruction) by deferring the set here until after you've iterated state

Comment on lines 360 to 366
/*
* Visit if branches separately -- we don't want
* to promote a use in the alternate branch because
* of an interposing instruction in the consequent branch,
* because it's not actually interposing at runtime.
* After finishing the branches, merge their states.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

this makes sense, you want to merge the state for the outer block context, but don't want to merge the state of the locals per branch.

can you add some more fixtures to stress test this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It turns out I don't think I can construct an example that exercises this behavior--this would require a temporary to be defined before an if terminal and then used within one branch of the terminal.

I'm inclined to drop this code path because of this--even if I'm wrong and we can construct an example that exercises this, the only consequence would be an unnecessarily promoted temporary.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense!

…odegen reordering over side-effectful operations"


This PR addresses the issue recorded [here](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-codegen-inline-iife.ts), where codegen reorders instructions by moving a temporary across a promoted temporary (or an instruction whose lvalue has been pruned) in an unsafe way.

We fix this by adding a new sub-pass to PromoteUsedTemporaries which looks for cases where the definition and the use of an unpromoted temporary, whose value could potentially be mutated, have an instruction in between them that 1. can cause mutation and 2. will be emitted as a separate statement. For our purposes, 1. means function calls and stores, and 2. means either that we've pruned the lvalue or that we've promoted the identifier (or technically that the identifier was named in the first place, but I don't think we could be reodering across it in the first place for this case). If we find such a temporary with an interposing mutative instruction, we'll promote it to a temporary as well.


[ghstack-poisoned]
mvitousek added a commit that referenced this pull request Aug 2, 2024
…dering over side-effectful operations

ghstack-source-id: 0907f8950bce5e241aa65c7691749914add70905
Pull Request resolved: #30554
Copy link
Contributor

@mofeiZ mofeiZ left a comment

Choose a reason for hiding this comment

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

Logic overall makes sense to me, pretty clever! Is the underlying intuition here that instructions are emitted in the correct order (e.g. IR order which follows source-code evaluation semantics) except for cases in which Forget adds statements, which effectively hoists the statement / rvals used by the statement to be emitted out of order?

I had a bit of trouble following the this.#consts tracking and constStore, but I think I now understand why this is needed (writing down some notes to check my understanding).

Checking constStore when handling non-reorderable instructions makes sense -- const stores are reorderable, but reassignments are not (overall logic seems similar to memory barriers).

[0]: $0 = ... // preEntry = [$0] (rvals evaluated before instruction [1])
[1]: ...      // <-- non-reorderable instruction that gets codegenned to
              // a statement / expressionStatement (e.g. CallExpression)
[2]: <use $0> // later reference to rval should promote $0 to a named temporary

this.#consts track const-bindings to allow reordering of any loads from a const-binding decl, e.g.

[0]: StoreLocal const x$0 = ...
[1]: $1 = LoadLocal x$0
[2]: <non-reorderable / fence instruction>
[3]: <use $1>  // No need to promote $1 since x$0 is a constant binding!

@mofeiZ
Copy link
Contributor

mofeiZ commented Aug 2, 2024

It seems that the fixture results are pretty stable (no false positives!) -- I'd be curious whether we observe any snapshot differences when syncing internally. I've found syncs to be a pretty great stress test for these types of changes

@mvitousek
Copy link
Contributor Author

@mofeiZ tested this internally (P1510568787) and found that this promotes a lot of global loads, like global.prop in [this example(https://playground.react.dev/#N4Igzg9grgTgxgUxALhAMygOzgFwJYSYAEUYCAKgmDgBR6YAOAlEcADrFEwI6zEA8AQyIMYEBgF5gAcwA2EAEaDZAOlHiAvkQg4AFghhS0dRky0B6AHwcNIDUA). This is technically correct--what if the function call has an alias to global and mutates .prop? However, this is inconsistent with other behavior that Forget assumes around mutability of props on globals (see HIRBuilder.isReorderableExpression). I'm inclined to relax the promotion requirements around reads from globals--if you're using Forget and reading from a mutable global you're already probably in trouble 😅

…odegen reordering over side-effectful operations"


This PR addresses the issue recorded [here](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-codegen-inline-iife.ts), where codegen reorders instructions by moving a temporary across a promoted temporary (or an instruction whose lvalue has been pruned) in an unsafe way.

We fix this by adding a new sub-pass to PromoteUsedTemporaries which looks for cases where the definition and the use of an unpromoted temporary, whose value could potentially be mutated, have an instruction in between them that 1. can cause mutation and 2. will be emitted as a separate statement. For our purposes, 1. means function calls and stores, and 2. means either that we've pruned the lvalue or that we've promoted the identifier (or technically that the identifier was named in the first place, but I don't think we could be reodering across it in the first place for this case). If we find such a temporary with an interposing mutative instruction, we'll promote it to a temporary as well.


[ghstack-poisoned]
mvitousek added a commit that referenced this pull request Aug 2, 2024
…dering over side-effectful operations

ghstack-source-id: 01c37b2e68b235c0cb9aada2f7062919d0d873db
Pull Request resolved: #30554
@josephsavona
Copy link
Contributor

I'm inclined to relax the promotion requirements around reads from globals--if you're using Forget and reading from a mutable global you're already probably in trouble

agreed, let’s relax the promotion requirements. We already freely reorder global accesses in ConstantPropagation.

…odegen reordering over side-effectful operations"


This PR addresses the issue recorded [here](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-codegen-inline-iife.ts), where codegen reorders instructions by moving a temporary across a promoted temporary (or an instruction whose lvalue has been pruned) in an unsafe way.

We fix this by adding a new sub-pass to PromoteUsedTemporaries which looks for cases where the definition and the use of an unpromoted temporary, whose value could potentially be mutated, have an instruction in between them that 1. can cause mutation and 2. will be emitted as a separate statement. For our purposes, 1. means function calls and stores, and 2. means either that we've pruned the lvalue or that we've promoted the identifier (or technically that the identifier was named in the first place, but I don't think we could be reodering across it in the first place for this case). If we find such a temporary with an interposing mutative instruction, we'll promote it to a temporary as well.


[ghstack-poisoned]
mvitousek added a commit that referenced this pull request Aug 12, 2024
…dering over side-effectful operations

ghstack-source-id: 639191e63a0d2b4290d1265a2da12fb17de750d9
Pull Request resolved: #30554
@mvitousek mvitousek merged commit c509bb6 into gh/mvitousek/18/base Aug 12, 2024
18 of 19 checks passed
@mvitousek mvitousek deleted the gh/mvitousek/18/head branch August 12, 2024 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants