Skip to content

Commit e4bfcd2

Browse files
authored
Update Heap2Local for struct RMW and cmpxchg (#7222)
Update the escape analysis to note that RMW modification values and cmpxchg replacement values can escape, but other operands do not escape. When the ref operands are being replaced with locals, lower the RMW and cmpxchg ops to the corresponding local and binary operations, being careful to use scratch locals necessary to prevent reordering problems. Add tests in a new file that will be ignored by the fuzzer until we have better fuzzing support for RMW and cmpxchg ops.
1 parent f9dd59e commit e4bfcd2

File tree

3 files changed

+854
-0
lines changed

3 files changed

+854
-0
lines changed

scripts/test/fuzzing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
'gc-atomics.wast',
8383
'gc-atomics-null-refs.wast',
8484
'shared-structs.wast',
85+
'heap2local-rmw.wast',
8586
# contains too many segments to run in a wasm VM
8687
'limit-segments_disable-bulk-memory.wast',
8788
# https://github.com/WebAssembly/binaryen/issues/7176

src/passes/Heap2Local.cpp

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
// This optimization focuses on such cases.
151151
//
152152

153+
#include "ir/abstract.h"
153154
#include "ir/bits.h"
154155
#include "ir/branch-utils.h"
155156
#include "ir/eh-utils.h"
@@ -414,6 +415,18 @@ struct EscapeAnalyzer {
414415
escapes = false;
415416
fullyConsumes = true;
416417
}
418+
void visitStructRMW(StructRMW* curr) {
419+
if (curr->ref == child) {
420+
escapes = false;
421+
fullyConsumes = true;
422+
}
423+
}
424+
void visitStructCmpxchg(StructCmpxchg* curr) {
425+
if (curr->ref == child || curr->expected == child) {
426+
escapes = false;
427+
fullyConsumes = true;
428+
}
429+
}
417430
void visitArraySet(ArraySet* curr) {
418431
if (!curr->index->is<Const>()) {
419432
// Array operations on nonconstant indexes do not escape in the normal
@@ -891,6 +904,132 @@ struct Struct2Local : PostWalker<Struct2Local> {
891904
}
892905
replaceCurrent(builder.blockify(replacement, value));
893906
}
907+
908+
void visitStructRMW(StructRMW* curr) {
909+
if (analyzer.getInteraction(curr) == ParentChildInteraction::None) {
910+
return;
911+
}
912+
913+
auto& field = fields[curr->index];
914+
auto type = curr->type;
915+
assert(type == field.type);
916+
assert(!field.isPacked());
917+
918+
// We need a scratch local to hold the old, unmodified field value while we
919+
// update the original local with the modified value. We also need another
920+
// scratch local to hold the evaluated modification value while we set the
921+
// first scratch local in case the evaluation of the modification value ends
922+
// up changing the field value. This is similar to the scratch locals used
923+
// for struct.new.
924+
auto oldScratch = builder.addVar(func, type);
925+
auto valScratch = builder.addVar(func, type);
926+
auto local = localIndexes[curr->index];
927+
928+
auto* block =
929+
builder.makeSequence(builder.makeDrop(curr->ref),
930+
builder.makeLocalSet(valScratch, curr->value));
931+
932+
// Stash the old value to return.
933+
block->list.push_back(
934+
builder.makeLocalSet(oldScratch, builder.makeLocalGet(local, type)));
935+
936+
// Store the updated value.
937+
Expression* newVal = nullptr;
938+
if (curr->op == RMWXchg) {
939+
newVal = builder.makeLocalGet(valScratch, type);
940+
} else {
941+
Abstract::Op binop = Abstract::Add;
942+
switch (curr->op) {
943+
case RMWAdd:
944+
binop = Abstract::Add;
945+
break;
946+
case RMWSub:
947+
binop = Abstract::Sub;
948+
break;
949+
case RMWAnd:
950+
binop = Abstract::And;
951+
break;
952+
case RMWOr:
953+
binop = Abstract::Or;
954+
break;
955+
case RMWXor:
956+
binop = Abstract::Xor;
957+
break;
958+
case RMWXchg:
959+
WASM_UNREACHABLE("unexpected op");
960+
}
961+
newVal = builder.makeBinary(Abstract::getBinary(type, binop),
962+
builder.makeLocalGet(local, type),
963+
builder.makeLocalGet(valScratch, type));
964+
}
965+
block->list.push_back(builder.makeLocalSet(local, newVal));
966+
967+
// See the notes on seqcst struct.get and struct.set.
968+
if (curr->order == MemoryOrder::SeqCst) {
969+
block->list.push_back(builder.makeAtomicFence());
970+
}
971+
972+
// Unstash the old value.
973+
block->list.push_back(builder.makeLocalGet(oldScratch, type));
974+
block->type = type;
975+
replaceCurrent(block);
976+
}
977+
978+
void visitStructCmpxchg(StructCmpxchg* curr) {
979+
if (analyzer.getInteraction(curr->ref) != ParentChildInteraction::Flows) {
980+
// The allocation can't flow into `replacement` if we've made it this far,
981+
// but it might flow into `expected`, in which case we don't need to do
982+
// anything because we would still be performing the cmpxchg on a real
983+
// struct. We only need to replace the cmpxchg if the ref is being
984+
// replaced with locals.
985+
return;
986+
}
987+
988+
auto& field = fields[curr->index];
989+
auto type = curr->type;
990+
assert(type == field.type);
991+
assert(!field.isPacked());
992+
993+
// Hold everything in scratch locals, just like for other RMW ops and
994+
// struct.new.
995+
auto oldScratch = builder.addVar(func, type);
996+
auto expectedScratch = builder.addVar(func, type);
997+
auto replacementScratch = builder.addVar(func, type);
998+
auto local = localIndexes[curr->index];
999+
1000+
auto* block = builder.makeBlock(
1001+
{builder.makeDrop(curr->ref),
1002+
builder.makeLocalSet(expectedScratch, curr->expected),
1003+
builder.makeLocalSet(replacementScratch, curr->replacement),
1004+
builder.makeLocalSet(oldScratch, builder.makeLocalGet(local, type))});
1005+
1006+
// Create the check for whether we should do the exchange.
1007+
auto* lhs = builder.makeLocalGet(local, type);
1008+
auto* rhs = builder.makeLocalGet(expectedScratch, type);
1009+
Expression* pred;
1010+
if (type.isRef()) {
1011+
pred = builder.makeRefEq(lhs, rhs);
1012+
} else {
1013+
pred =
1014+
builder.makeBinary(Abstract::getBinary(type, Abstract::Eq), lhs, rhs);
1015+
}
1016+
1017+
// The conditional exchange.
1018+
block->list.push_back(
1019+
builder.makeIf(pred,
1020+
builder.makeLocalSet(
1021+
local, builder.makeLocalGet(replacementScratch, type))));
1022+
1023+
// See the notes on seqcst struct.get and struct.set.
1024+
if (curr->order == MemoryOrder::SeqCst) {
1025+
block->list.push_back(builder.makeAtomicFence());
1026+
}
1027+
1028+
// Unstash the old value.
1029+
block->list.push_back(builder.makeLocalGet(oldScratch, type));
1030+
block->type = type;
1031+
replaceCurrent(block);
1032+
}
8941033
};
8951034

8961035
// An optimizer that handles the rewriting to turn a nonescaping array

0 commit comments

Comments
 (0)