Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/test/fuzzing.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
'vacuum-removable-if-unused.wast',
'vacuum-removable-if-unused-func.wast',
'strip-toolchain-annotations-func.wast',
'idempotent.wast',
'optimize-instructions_idempotent.wast',
# Not fully implemented.
'waitqueue.wast',
]
Expand Down
7 changes: 5 additions & 2 deletions src/ir/intrinsics.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class Intrinsics {
std::vector<Name> getJSCalledFunctions();

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

// Get the code annotations for a function itself.
CodeAnnotation getAnnotations(Function* func) {
static CodeAnnotation getAnnotations(Function* func) {
return getAnnotations(nullptr, func);
}

Expand Down Expand Up @@ -155,6 +155,9 @@ class Intrinsics {
if (!ret.jsCalled) {
ret.jsCalled = funcAnnotations.jsCalled;
}
if (!ret.idempotent) {
ret.idempotent = funcAnnotations.idempotent;
}
}

return ret;
Expand Down
18 changes: 15 additions & 3 deletions src/ir/properties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#include "ir/properties.h"
#include "ir/intrinsics.h"
#include "wasm-traversal.h"

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

void visitCall(Call* curr) {
// TODO: We could in principle look at the called function to see if it is
// If the called function is idempotent, then it does not generate new
// values on each call.
Comment on lines +29 to +30
Copy link
Member

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.

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 think that's already in the definition of generative?

// A "generative" expression is one that can generate different results for the
// same inputs, and that difference is *not* explained by other expressions that

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.

if (Intrinsics(*getModule())
.getCallAnnotations(curr, getFunction())
.idempotent) {
return;
}
// TODO: We could look at the called function's contents to see if it is
// generative. To do that we'd need to compute generativity like we
// compute global effects (we can't just peek from here, as the
// other function might be modified in parallel).
Expand All @@ -43,15 +51,19 @@ struct GenerativityScanner : public PostWalker<GenerativityScanner> {

} // anonymous namespace

bool isGenerative(Expression* curr) {
bool isGenerative(Expression* curr, Function* func, Module& wasm) {
GenerativityScanner scanner;
scanner.setFunction(func);
scanner.setModule(&wasm);
scanner.walk(curr);
return scanner.generative;
}

// As above, but only checks |curr| and not children.
bool isShallowlyGenerative(Expression* curr) {
bool isShallowlyGenerative(Expression* curr, Function* func, Module& wasm) {
GenerativityScanner scanner;
scanner.setFunction(func);
scanner.setModule(&wasm);
scanner.visit(curr);
return scanner.generative;
}
Expand Down
4 changes: 2 additions & 2 deletions src/ir/properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -597,10 +597,10 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) {
// the latter because calls are already handled best in other manners (using
// EffectAnalyzer).
//
bool isGenerative(Expression* curr);
bool isGenerative(Expression* curr, Function* func, Module& wasm);

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

// Whether this expression is valid in a context where WebAssembly requires a
// constant expression, such as a global initializer.
Expand Down
2 changes: 2 additions & 0 deletions src/parser/contexts.h
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,8 @@ struct AnnotationParserCtx {
ret.removableIfUnused = true;
} else if (a.kind == Annotations::JSCalledHint) {
ret.jsCalled = true;
} else if (a.kind == Annotations::IdempotentHint) {
ret.idempotent = true;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/passes/LocalCSE.cpp
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 areEqualAndFoldable part here. That and OnceReduction I can do later.

Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ struct Scanner
// We also cannot optimize away something that is intrinsically
// nondeterministic: even if it has no side effects, if it may return a
// different result each time, and then we cannot optimize away repeats.
if (Properties::isShallowlyGenerative(curr)) {
if (Properties::isShallowlyGenerative(curr, getFunction(), *getModule())) {
return false;
}

Expand Down
84 changes: 56 additions & 28 deletions src/passes/OptimizeInstructions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Expand Down
6 changes: 6 additions & 0 deletions src/passes/Print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2803,6 +2803,12 @@ void PrintSExpression::printCodeAnnotations(Expression* curr) {
restoreNormalColor(o);
doIndent(o, indent);
}
if (annotation.idempotent) {
Colors::grey(o);
o << "(@" << Annotations::IdempotentHint << ")\n";
restoreNormalColor(o);
doIndent(o, indent);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/passes/StripToolchainAnnotations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct StripToolchainAnnotations
auto& annotation = iter->second;
annotation.removableIfUnused = false;
annotation.jsCalled = false;
annotation.idempotent = false;

// If nothing remains, remove the entire annotation.
if (annotation == CodeAnnotation()) {
Expand Down
1 change: 1 addition & 0 deletions src/wasm-annotations.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ extern const Name BranchHint;
extern const Name InlineHint;
extern const Name RemovableIfUnusedHint;
extern const Name JSCalledHint;
extern const Name IdempotentHint;

} // namespace wasm::Annotations

Expand Down
2 changes: 2 additions & 0 deletions src/wasm-binary.h
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,7 @@ class WasmBinaryWriter {
std::optional<BufferWithRandomAccess> getInlineHintsBuffer();
std::optional<BufferWithRandomAccess> getRemovableIfUnusedHintsBuffer();
std::optional<BufferWithRandomAccess> getJSCalledHintsBuffer();
std::optional<BufferWithRandomAccess> getIdempotentHintsBuffer();

// helpers
void writeInlineString(std::string_view name);
Expand Down Expand Up @@ -1738,6 +1739,7 @@ class WasmBinaryReader {
void readInlineHints(size_t payloadLen);
void readRemovableIfUnusedHints(size_t payloadLen);
void readJSCalledHints(size_t payloadLen);
void readIdempotentHints(size_t payloadLen);

std::tuple<Address, Address, Index, MemoryOrder>
readMemoryAccess(bool isAtomic, bool isRMW);
Expand Down
15 changes: 14 additions & 1 deletion src/wasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think this would be a nice cleanup anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Expand Down
Loading
Loading