Skip to content

Commit 665d6f7

Browse files
authored
Intrinsic: idempotent (#8354)
An idempotent function may do work when called, but if called again with the same parameters, it does no work and it returns the same value (if it returns a value). This uses the new intrinsic in OptimizeInstructions. More things are possible too. See discussion in #7574
1 parent 9656b8e commit 665d6f7

18 files changed

+547
-49
lines changed

scripts/test/fuzzing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@
116116
'vacuum-removable-if-unused.wast',
117117
'vacuum-removable-if-unused-func.wast',
118118
'strip-toolchain-annotations-func.wast',
119+
'idempotent.wast',
120+
'optimize-instructions_idempotent.wast',
119121
# Not fully implemented.
120122
'waitqueue.wast',
121123
]

src/ir/intrinsics.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class Intrinsics {
116116
std::vector<Name> getJSCalledFunctions();
117117

118118
// Get the code annotations for an expression in a function.
119-
CodeAnnotation getAnnotations(Expression* curr, Function* func) {
119+
static CodeAnnotation getAnnotations(Expression* curr, Function* func) {
120120
auto& annotations = func->codeAnnotations;
121121
auto iter = annotations.find(curr);
122122
if (iter != annotations.end()) {
@@ -126,7 +126,7 @@ class Intrinsics {
126126
}
127127

128128
// Get the code annotations for a function itself.
129-
CodeAnnotation getAnnotations(Function* func) {
129+
static CodeAnnotation getAnnotations(Function* func) {
130130
return getAnnotations(nullptr, func);
131131
}
132132

@@ -165,6 +165,9 @@ class Intrinsics {
165165
if (!ret.jsCalled) {
166166
ret.jsCalled = funcAnnotations.jsCalled;
167167
}
168+
if (!ret.idempotent) {
169+
ret.idempotent = funcAnnotations.idempotent;
170+
}
168171
}
169172

170173
return ret;

src/ir/properties.cpp

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
#include "ir/properties.h"
18+
#include "ir/intrinsics.h"
1819
#include "wasm-traversal.h"
1920

2021
namespace wasm::Properties {
@@ -25,7 +26,14 @@ struct GenerativityScanner : public PostWalker<GenerativityScanner> {
2526
bool generative = false;
2627

2728
void visitCall(Call* curr) {
28-
// TODO: We could in principle look at the called function to see if it is
29+
// If the called function is idempotent, then it does not generate new
30+
// values on each call.
31+
if (Intrinsics(*getModule())
32+
.getCallAnnotations(curr, getFunction())
33+
.idempotent) {
34+
return;
35+
}
36+
// TODO: We could look at the called function's contents to see if it is
2937
// generative. To do that we'd need to compute generativity like we
3038
// compute global effects (we can't just peek from here, as the
3139
// other function might be modified in parallel).
@@ -43,15 +51,19 @@ struct GenerativityScanner : public PostWalker<GenerativityScanner> {
4351

4452
} // anonymous namespace
4553

46-
bool isGenerative(Expression* curr) {
54+
bool isGenerative(Expression* curr, Function* func, Module& wasm) {
4755
GenerativityScanner scanner;
56+
scanner.setFunction(func);
57+
scanner.setModule(&wasm);
4858
scanner.walk(curr);
4959
return scanner.generative;
5060
}
5161

5262
// As above, but only checks |curr| and not children.
53-
bool isShallowlyGenerative(Expression* curr) {
63+
bool isShallowlyGenerative(Expression* curr, Function* func, Module& wasm) {
5464
GenerativityScanner scanner;
65+
scanner.setFunction(func);
66+
scanner.setModule(&wasm);
5567
scanner.visit(curr);
5668
return scanner.generative;
5769
}

src/ir/properties.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -597,10 +597,10 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) {
597597
// the latter because calls are already handled best in other manners (using
598598
// EffectAnalyzer).
599599
//
600-
bool isGenerative(Expression* curr);
600+
bool isGenerative(Expression* curr, Function* func, Module& wasm);
601601

602602
// As above, but only checks |curr| and not children.
603-
bool isShallowlyGenerative(Expression* curr);
603+
bool isShallowlyGenerative(Expression* curr, Function* func, Module& wasm);
604604

605605
// Whether this expression is valid in a context where WebAssembly requires a
606606
// constant expression, such as a global initializer.

src/parser/contexts.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,8 @@ struct AnnotationParserCtx {
13211321
ret.removableIfUnused = true;
13221322
} else if (a.kind == Annotations::JSCalledHint) {
13231323
ret.jsCalled = true;
1324+
} else if (a.kind == Annotations::IdempotentHint) {
1325+
ret.idempotent = true;
13241326
}
13251327
}
13261328

src/passes/LocalCSE.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ struct Scanner
398398
// We also cannot optimize away something that is intrinsically
399399
// nondeterministic: even if it has no side effects, if it may return a
400400
// different result each time, and then we cannot optimize away repeats.
401-
if (Properties::isShallowlyGenerative(curr)) {
401+
if (Properties::isShallowlyGenerative(curr, getFunction(), *getModule())) {
402402
return false;
403403
}
404404

src/passes/OptimizeInstructions.cpp

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,8 +2822,12 @@ struct OptimizeInstructions
28222822
}
28232823

28242824
// To be equal, they must also be known to return the same result
2825-
// deterministically.
2826-
return !Properties::isGenerative(left);
2825+
// deterministically. We check the right side, as if the right is marked
2826+
// idempotent, that is enough (that tells us it does not generate a new
2827+
// value; logically, of course, as left is equal to right, they are calling
2828+
// the same thing, so it is odd to only annotate one, but this is consistent
2829+
// and easy to check).
2830+
return !Properties::isGenerative(right, getFunction(), *getModule());
28272831
}
28282832

28292833
// Check if two consecutive inputs to an instruction are equal and can also be
@@ -2841,9 +2845,51 @@ struct OptimizeInstructions
28412845
}
28422846

28432847
// To fold the right side into the left, it must have no effects.
2844-
if (EffectAnalyzer(getPassOptions(), *getModule(), right)
2845-
.hasUnremovableSideEffects()) {
2846-
return false;
2848+
auto rightMightHaveEffects = true;
2849+
if (auto* call = right->dynCast<Call>()) {
2850+
// If these are a pair of idempotent calls, then the second has no
2851+
// effects. (We didn't check if left is a call, but the equality check
2852+
// below does that.)
2853+
if (Intrinsics(*getModule())
2854+
.getCallAnnotations(call, getFunction())
2855+
.idempotent) {
2856+
// We must still check for effects in the parameters. Imagine that we
2857+
// have
2858+
//
2859+
// (call $idempotent (global.get $g))
2860+
// (call $idempotent (global.get $g))
2861+
//
2862+
// Then the first call has effects, and those might alter $g if the
2863+
// global is mutable. That is, all that idempotency tells us is that
2864+
// the second call has no effects, but its parameters can still have
2865+
// read effects that interact. Also, the parameter might have write
2866+
// effects,
2867+
//
2868+
// (call $idempotent (call $other))
2869+
//
2870+
// We must check that as well.
2871+
EffectAnalyzer childEffects(getPassOptions(), *getModule());
2872+
for (auto* child : call->operands) {
2873+
childEffects.walk(child);
2874+
}
2875+
if (childEffects.hasUnremovableSideEffects()) {
2876+
return false;
2877+
}
2878+
ShallowEffectAnalyzer parentEffects(
2879+
getPassOptions(), *getModule(), call);
2880+
if (parentEffects.invalidates(childEffects)) {
2881+
return false;
2882+
}
2883+
// No effects are possible.
2884+
rightMightHaveEffects = false;
2885+
}
2886+
}
2887+
if (rightMightHaveEffects) {
2888+
// So far it looks like right has effects, so check fully.
2889+
if (EffectAnalyzer(getPassOptions(), *getModule(), right)
2890+
.hasUnremovableSideEffects()) {
2891+
return false;
2892+
}
28472893
}
28482894

28492895
return areConsecutiveInputsEqual(left, right);

src/passes/Print.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2803,6 +2803,12 @@ void PrintSExpression::printCodeAnnotations(Expression* curr) {
28032803
restoreNormalColor(o);
28042804
doIndent(o, indent);
28052805
}
2806+
if (annotation.idempotent) {
2807+
Colors::grey(o);
2808+
o << "(@" << Annotations::IdempotentHint << ")\n";
2809+
restoreNormalColor(o);
2810+
doIndent(o, indent);
2811+
}
28062812
}
28072813
}
28082814

src/passes/StripToolchainAnnotations.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ struct StripToolchainAnnotations
4444
auto& annotation = iter->second;
4545
annotation.removableIfUnused = false;
4646
annotation.jsCalled = false;
47+
annotation.idempotent = false;
4748

4849
// If nothing remains, remove the entire annotation.
4950
if (annotation == CodeAnnotation()) {

src/wasm-annotations.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ extern const Name BranchHint;
2929
extern const Name InlineHint;
3030
extern const Name RemovableIfUnusedHint;
3131
extern const Name JSCalledHint;
32+
extern const Name IdempotentHint;
3233

3334
} // namespace wasm::Annotations
3435

0 commit comments

Comments
 (0)