-
Notifications
You must be signed in to change notification settings - Fork 837
Intrinsic: idempotent #8354
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
base: main
Are you sure you want to change the base?
Intrinsic: idempotent #8354
Changes from all commits
df4c8aa
0b326e2
a95d3ed
d66a33a
881576b
48d8384
c769ed0
bcd5dac
ef44cc2
148fa86
80ad047
6493931
af34e7b
97a7a52
9b1a94c
b38290d
001edbe
868a60c
d446f0c
357e519
314443f
0f429e4
65f10b3
f791aa6
ed0dc57
ef4d2f0
fb1df27
9f1fc53
2f2d043
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this enough to write CSE tests that show idempotent function calls being deduplicated?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CSE needs to actually find the two calls, and only then can it infer that the second is removable. So this does require logic in each pass, like the |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2822,35 +2822,17 @@ struct OptimizeInstructions | |
| } | ||
|
|
||
| // To be equal, they must also be known to return the same result | ||
| // deterministically. | ||
| return !Properties::isGenerative(left); | ||
| } | ||
|
|
||
| // Similar to areConsecutiveInputsEqual() but also checks if we can remove | ||
| // them (but we do not assume the caller will always remove them). | ||
| bool areConsecutiveInputsEqualAndRemovable(Expression* left, | ||
| Expression* right) { | ||
| // First, check for side effects. If there are any, then we can't even | ||
| // assume things like local.get's of the same index being identical. (It is | ||
| // also ok to have removable side effects here, see the function | ||
| // description.) | ||
| auto& passOptions = getPassOptions(); | ||
| if (EffectAnalyzer(passOptions, *getModule(), left) | ||
| .hasUnremovableSideEffects() || | ||
| EffectAnalyzer(passOptions, *getModule(), right) | ||
| .hasUnremovableSideEffects()) { | ||
| return false; | ||
| } | ||
|
|
||
| return areConsecutiveInputsEqual(left, right); | ||
| // deterministically. We check the right side, as if the right is marked | ||
| // idempotent, that is enough (that tells us it does not generate a new | ||
| // value; logically, of course, as left is equal to right, they are calling | ||
| // the same thing, so it is odd to only annotate one, but this is consistent | ||
| // and easy to check). | ||
| return !Properties::isGenerative(right, getFunction(), *getModule()); | ||
| } | ||
|
|
||
| // Check if two consecutive inputs to an instruction are equal and can also be | ||
| // folded into the first of the two (but we do not assume the caller will | ||
| // always fold them). This is similar to areConsecutiveInputsEqualAndRemovable | ||
| // but also identifies reads from the same local variable when the first of | ||
| // them is a "tee" operation and the second is a get (in which case, it is | ||
| // fine to remove the get, but not the tee). | ||
| // always fold them). | ||
| // | ||
| // The inputs here must be consecutive, but it is also ok to have code with no | ||
| // side effects at all in the middle. For example, a Const in between is ok. | ||
|
|
@@ -2862,9 +2844,55 @@ struct OptimizeInstructions | |
| return true; | ||
| } | ||
|
|
||
| // stronger property than we need - we can not only fold | ||
| // them but remove them entirely. | ||
| return areConsecutiveInputsEqualAndRemovable(left, right); | ||
| // To fold the right side into the left, it must have no effects. | ||
| auto rightMightHaveEffects = true; | ||
| if (auto* call = right->dynCast<Call>()) { | ||
| // If these are a pair of idempotent calls, then the second has no | ||
| // effects. (We didn't check if left is a call, but the equality check | ||
| // below does that.) | ||
| if (Intrinsics(*getModule()) | ||
| .getCallAnnotations(call, getFunction()) | ||
| .idempotent) { | ||
| // We must still check for effects in the parameters. Imagine that we | ||
| // have | ||
| // | ||
| // (call $idempotent (global.get $g)) | ||
| // (call $idempotent (global.get $g)) | ||
| // | ||
| // Then the first call has effects, and those might alter $g if the | ||
| // global is mutable. That is, all that idempotency tells us is that | ||
| // the second call has no effects, but its parameters can still have | ||
| // read effects that interact. Also, the parameter might have write | ||
| // effects, | ||
| // | ||
| // (call $idempotent (call $other)) | ||
| // | ||
| // We must check that as well. | ||
| EffectAnalyzer childEffects(getPassOptions(), *getModule()); | ||
| for (auto* child : call->operands) { | ||
| childEffects.walk(child); | ||
| } | ||
| if (childEffects.hasUnremovableSideEffects()) { | ||
| return false; | ||
| } | ||
| ShallowEffectAnalyzer parentEffects( | ||
| getPassOptions(), *getModule(), call); | ||
| if (parentEffects.invalidates(childEffects)) { | ||
| return false; | ||
| } | ||
| // No effects are possible. | ||
| rightMightHaveEffects = false; | ||
|
Comment on lines
+2883
to
+2884
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also expect us to have to check that the child values match here. Do we not?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We check that the entire expressions match, including children, on line 2891. It is just that it seems more efficient to do it in this order (the equality check scans both inputs, not just one, so early exit can avoid scanning half the data). |
||
| } | ||
| } | ||
| if (rightMightHaveEffects) { | ||
| // So far it looks like right has effects, so check fully. | ||
| if (EffectAnalyzer(getPassOptions(), *getModule(), right) | ||
| .hasUnremovableSideEffects()) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return areConsecutiveInputsEqual(left, right); | ||
| } | ||
|
|
||
| // Canonicalizing the order of a symmetric binary helps us | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2258,10 +2258,16 @@ struct CodeAnnotation { | |
| // identity does not matter for such functions. | ||
| bool jsCalled = false; | ||
|
|
||
| // A function that may do something on the first call, but all subsequent | ||
| // calls with the same parameters can be assumed to have no effects. If a | ||
| // value is returned, it will be the same value as returned earlier (for the | ||
| // same parameters). | ||
| bool idempotent = false; | ||
|
|
||
| bool operator==(const CodeAnnotation& other) const { | ||
| return branchLikely == other.branchLikely && inline_ == other.inline_ && | ||
| removableIfUnused == other.removableIfUnused && | ||
| jsCalled == other.jsCalled; | ||
| jsCalled == other.jsCalled && idempotent == other.idempotent; | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -2322,6 +2328,13 @@ class Function : public Importable { | |
| // the 0 byte offset in the spec. As with debug info, we do not store these on | ||
| // Expressions as we assume most instances are unannotated, and do not want to | ||
| // add constant memory overhead. | ||
| // XXX As an unordered map, if this is modified by one thread, another should | ||
| // not be reading it. That should not happen atm - all annotations are | ||
| // set up in dedicated passes or in the binary reader - but if one pass | ||
| // could add an expression annotation, another should not at the same time | ||
| // read the function-level annotations, even though that is natural to do. | ||
| // We may want to move the function-level annotations to a dedicated | ||
| // field outside the map. | ||
|
Comment on lines
+2336
to
+2337
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think this would be a nice cleanup anyway.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sg, will do it soon. |
||
| std::unordered_map<Expression*, CodeAnnotation> codeAnnotations; | ||
|
|
||
| // The effects for this function, if they have been computed. We use a shared | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct. An idempotent function could return new values on each call as long as it gets different parameters each time (for example if it's either creating new values or looking them up in a cache). It seems like this would cause problems for users of
isShallowlyGenerative, at least.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's already in the definition of generative?
binaryen/src/ir/properties.h
Lines 558 to 559 in 4efcdfe
If the inputs change, all bets are off.
Users of isShallowlyGenerative need to look at children, correct. LocalCSE does that, basically the "shallow" version is an incremental one, more efficient sometimes, but the children must be scanned as well.