[WebAssembly] Support acquire-release atomics in CodeGen (#184900)

Set the correct memory ordering for relaxed atomics after ISel. This
allows
SelectionDAG to keep the simple generic selection for target-independent
AtomicLoad nodes, but keeps the ordering immediate correct in the MIR.
Notably, the MachineMemOperand still has the original memory ordering
and MIR passes would use that rather than the ordering immedate to make
their code motion decisions (if we had any for Wasm, which we don't).
This commit is contained in:
Derek Schuff 2026-03-16 09:11:54 -07:00 committed by GitHub
parent 6b1ad6bbfd
commit 18d85a33cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1133 additions and 139 deletions

View file

@ -16,6 +16,7 @@
#include "WebAssemblyISelLowering.h"
#include "WebAssemblyTargetMachine.h"
#include "WebAssemblyUtilities.h"
#include "llvm/BinaryFormat/Wasm.h"
#include "llvm/CodeGen/MachineFrameInfo.h"
#include "llvm/CodeGen/SelectionDAGISel.h"
#include "llvm/CodeGen/WasmEHFuncInfo.h"
@ -69,6 +70,12 @@ public:
bool SelectAddrOperands32(SDValue Op, SDValue &Offset, SDValue &Addr);
bool SelectAddrOperands64(SDValue Op, SDValue &Offset, SDValue &Addr);
bool SelectAtomicAddrOperands(SDNode *Op, SDValue N, SDValue &Offset,
SDValue &Addr, SDValue &Order, bool Is64);
bool SelectAtomicAddrOperands32(SDNode *Op, SDValue N, SDValue &Offset,
SDValue &Addr, SDValue &Order);
bool SelectAtomicAddrOperands64(SDNode *Op, SDValue N, SDValue &Offset,
SDValue &Addr, SDValue &Order);
// Include the pieces autogenerated from the target description.
#include "WebAssemblyGenDAGISel.inc"
@ -174,6 +181,25 @@ static APInt encodeFunctionSignature(SelectionDAG *DAG, SDLoc &DL,
return Sig;
}
static unsigned getWebAssemblyMemoryOrder(AtomicOrdering Ordering) {
unsigned OrderVal = wasm::WASM_MEM_ORDER_SEQ_CST;
switch (Ordering) {
case AtomicOrdering::Unordered:
case AtomicOrdering::Monotonic:
case AtomicOrdering::Acquire:
case AtomicOrdering::Release:
case AtomicOrdering::AcquireRelease:
OrderVal = wasm::WASM_MEM_ORDER_ACQ_REL;
break;
case AtomicOrdering::SequentiallyConsistent:
OrderVal = wasm::WASM_MEM_ORDER_SEQ_CST;
break;
default:
llvm_unreachable("Invalid atomic ordering");
}
return OrderVal;
}
void WebAssemblyDAGToDAGISel::Select(SDNode *Node) {
// If we have a custom node, we already have selected!
if (Node->isMachineOpcode()) {
@ -186,7 +212,6 @@ void WebAssemblyDAGToDAGISel::Select(SDNode *Node) {
auto GlobalGetIns = PtrVT == MVT::i64 ? WebAssembly::GLOBAL_GET_I64
: WebAssembly::GLOBAL_GET_I32;
// Few custom selection stuff.
SDLoc DL(Node);
MachineFunction &MF = CurDAG->getMachineFunction();
switch (Node->getOpcode()) {
@ -207,17 +232,22 @@ void WebAssemblyDAGToDAGISel::Select(SDNode *Node) {
Node->getOperand(0) // inchain
);
break;
case SyncScope::System:
// Currently wasm only supports sequentially consistent atomics, so we
// always set the order to 0 (sequentially consistent).
case SyncScope::System: {
unsigned Order = wasm::WASM_MEM_ORDER_SEQ_CST;
if (MF.getSubtarget<WebAssemblySubtarget>().hasRelaxedAtomics()) {
auto Ordering =
static_cast<AtomicOrdering>(Node->getConstantOperandVal(1));
Order = getWebAssemblyMemoryOrder(Ordering);
}
Fence = CurDAG->getMachineNode(
WebAssembly::ATOMIC_FENCE,
DL, // debug loc
MVT::Other, // outchain type
CurDAG->getTargetConstant(0, DL, MVT::i32), // order
Node->getOperand(0) // inchain
DL, // debug loc
MVT::Other, // outchain type
CurDAG->getTargetConstant(Order, DL, MVT::i32), // order
Node->getOperand(0) // inchain
);
break;
}
default:
llvm_unreachable("Unknown scope!");
}
@ -548,6 +578,65 @@ bool WebAssemblyDAGToDAGISel::SelectAddrOperands64(SDValue Op, SDValue &Offset,
return SelectAddrOperands(MVT::i64, WebAssembly::CONST_I64, Op, Offset, Addr);
}
static MemSDNode *findMemSDNode(SDNode *N) {
while (N) {
if (auto *MemNode = dyn_cast<MemSDNode>(N))
return MemNode;
switch (N->getOpcode()) {
case ISD::ZERO_EXTEND:
case ISD::SIGN_EXTEND:
case ISD::ANY_EXTEND:
case ISD::SIGN_EXTEND_INREG:
case ISD::AssertZext:
case ISD::AssertSext:
case ISD::TRUNCATE:
case ISD::BITCAST:
case ISD::AND:
N = N->getOperand(0).getNode();
break;
default:
return nullptr;
}
}
return nullptr;
}
bool WebAssemblyDAGToDAGISel::SelectAtomicAddrOperands(SDNode *Op, SDValue N,
SDValue &Offset,
SDValue &Addr,
SDValue &Order,
bool Is64) {
auto *MemNode = findMemSDNode(Op);
if (!MemNode)
return false;
bool Match = Is64 ? SelectAddrOperands64(N, Offset, Addr)
: SelectAddrOperands32(N, Offset, Addr);
if (!Match)
return false;
auto Ordering = MemNode->getMergedOrdering();
unsigned OrderVal = wasm::WASM_MEM_ORDER_SEQ_CST;
if (Subtarget->hasRelaxedAtomics())
OrderVal = getWebAssemblyMemoryOrder(Ordering);
Order = CurDAG->getTargetConstant(OrderVal, SDLoc(Op), MVT::i32);
return true;
}
bool WebAssemblyDAGToDAGISel::SelectAtomicAddrOperands32(SDNode *Op, SDValue N,
SDValue &Offset,
SDValue &Addr,
SDValue &Order) {
return SelectAtomicAddrOperands(Op, N, Offset, Addr, Order, /*Is64=*/false);
}
bool WebAssemblyDAGToDAGISel::SelectAtomicAddrOperands64(SDNode *Op, SDValue N,
SDValue &Offset,
SDValue &Addr,
SDValue &Order) {
return SelectAtomicAddrOperands(Op, N, Offset, Addr, Order, /*Is64=*/true);
}
/// This pass converts a legalized DAG into a WebAssembly-specific DAG, ready
/// for instruction scheduling.
FunctionPass *llvm::createWebAssemblyISelDag(WebAssemblyTargetMachine &TM,

View file

@ -11,6 +11,13 @@
///
//===----------------------------------------------------------------------===//
def AtomicAddrOps32 : ComplexPattern<i32, 3, "SelectAtomicAddrOperands32"> {
let WantsRoot = 1;
}
def AtomicAddrOps64 : ComplexPattern<i64, 3, "SelectAtomicAddrOperands64"> {
let WantsRoot = 1;
}
let UseNamedOperandTable = 1 in
multiclass ATOMIC_I<dag oops_r, dag iops_r, dag oops_s, dag iops_s,
list<dag> pattern_r, string asmstr_r,
@ -150,11 +157,11 @@ multiclass AtomicLoad<WebAssemblyRegClass rc, string name, int atomic_op> {
}
multiclass AtomicLoadPat<ValueType ty, SDPatternOperator kind, string Name> {
def : Pat<(ty (kind (AddrOps32 offset32_op:$offset, I32:$addr))),
(!cast<NI>(Name # "_A32") 0, 0, offset32_op:$offset, I32:$addr)>,
def : Pat<(ty (kind (AtomicAddrOps32 offset32_op:$offset, I32:$addr, i32imm:$order))),
(!cast<NI>(Name # "_A32") $order, 0, offset32_op:$offset, I32:$addr)>,
Requires<[HasAddr32, HasAtomics]>;
def : Pat<(ty (kind (AddrOps64 offset64_op:$offset, I64:$addr))),
(!cast<NI>(Name # "_A64") 0, 0, offset64_op:$offset, I64:$addr)>,
def : Pat<(ty (kind (AtomicAddrOps64 offset64_op:$offset, I64:$addr, i32imm:$order))),
(!cast<NI>(Name # "_A64") $order, 0, offset64_op:$offset, I64:$addr)>,
Requires<[HasAddr64, HasAtomics]>;
}
@ -245,18 +252,12 @@ multiclass AtomicStore<WebAssemblyRegClass rc, string name, int atomic_op> {
defm ATOMIC_STORE_I32 : AtomicStore<I32, "i32.atomic.store", 0x17>;
defm ATOMIC_STORE_I64 : AtomicStore<I64, "i64.atomic.store", 0x18>;
// We used to need an 'atomic' version of store patterns because store and atomic_store
// nodes have different operand orders.
//
// TODO: This is no longer true and atomic_store and store patterns
// can be unified.
multiclass AStorePat<ValueType ty, PatFrag kind, string inst> {
def : Pat<(kind ty:$val, (AddrOps32 offset32_op:$offset, I32:$addr)),
(!cast<NI>(inst#_A32) 0, 0, $offset, $addr, $val)>,
def : Pat<(kind ty:$val, (AtomicAddrOps32 offset32_op:$offset, I32:$addr, i32imm:$order)),
(!cast<NI>(inst#_A32) $order, 0, $offset, $addr, $val)>,
Requires<[HasAddr32, HasAtomics]>;
def : Pat<(kind ty:$val, (AddrOps64 offset64_op:$offset, I64:$addr)),
(!cast<NI>(inst#_A64) 0, 0, $offset, $addr, $val)>,
def : Pat<(kind ty:$val, (AtomicAddrOps64 offset64_op:$offset, I64:$addr, i32imm:$order)),
(!cast<NI>(inst#_A64) $order, 0, $offset, $addr, $val)>,
Requires<[HasAddr64, HasAtomics]>;
}
defm : AStorePat<i32, atomic_store_32, "ATOMIC_STORE_I32">;
@ -393,11 +394,13 @@ defm ATOMIC_RMW32_U_XCHG_I64 :
WebAssemblyBinRMW<I64, "i64.atomic.rmw32.xchg_u", 0x47>;
multiclass BinRMWPat<ValueType ty, PatFrag kind, string inst> {
def : Pat<(ty (kind (AddrOps32 offset32_op:$offset, I32:$addr), ty:$val)),
(!cast<NI>(inst#_A32) 0, 0, $offset, $addr, $val)>,
def : Pat<(ty (kind (AtomicAddrOps32 offset32_op:$offset, I32:$addr,
i32imm:$order), ty:$val)),
(!cast<NI>(inst#_A32) $order, 0, $offset, $addr, $val)>,
Requires<[HasAddr32, HasAtomics]>;
def : Pat<(ty (kind (AddrOps64 offset64_op:$offset, I64:$addr), ty:$val)),
(!cast<NI>(inst#_A64) 0, 0, $offset, $addr, $val)>,
def : Pat<(ty (kind (AtomicAddrOps64 offset64_op:$offset, I64:$addr,
i32imm:$order), ty:$val)),
(!cast<NI>(inst#_A64) $order, 0, $offset, $addr, $val)>,
Requires<[HasAddr64, HasAtomics]>;
}
@ -536,11 +539,11 @@ defm ATOMIC_RMW32_U_CMPXCHG_I64 :
WebAssemblyTerRMW<I64, "i64.atomic.rmw32.cmpxchg_u", 0x4e>;
multiclass TerRMWPat<ValueType ty, PatFrag kind, string inst> {
def : Pat<(ty (kind (AddrOps32 offset32_op:$offset, I32:$addr), ty:$exp, ty:$new)),
(!cast<NI>(inst#_A32) 0, 0, $offset, $addr, $exp, $new)>,
def : Pat<(ty (kind (AtomicAddrOps32 offset32_op:$offset, I32:$addr, i32imm:$order), ty:$exp, ty:$new)),
(!cast<NI>(inst#_A32) $order, 0, $offset, $addr, $exp, $new)>,
Requires<[HasAddr32, HasAtomics]>;
def : Pat<(ty (kind (AddrOps64 offset64_op:$offset, I64:$addr), ty:$exp, ty:$new)),
(!cast<NI>(inst#_A64) 0, 0, $offset, $addr, $exp, $new)>,
def : Pat<(ty (kind (AtomicAddrOps64 offset64_op:$offset, I64:$addr, i32imm:$order), ty:$exp, ty:$new)),
(!cast<NI>(inst#_A64) $order, 0, $offset, $addr, $exp, $new)>,
Requires<[HasAddr64, HasAtomics]>;
}

View file

@ -1,36 +1,140 @@
; RUN: llc < %s | FileCheck %s --check-prefix NOATOMIC
; RUN: llc < %s -asm-verbose=false -wasm-disable-explicit-locals -wasm-keep-registers -mattr=+atomics | FileCheck %s
target triple = "wasm32-unknown-unknown"
; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
; RUN: llc < %s --mtriple=wasm32 | FileCheck %s --check-prefixes=NO-ATOMICS
; RUN: llc < %s --mtriple=wasm32 -mattr=+atomics | FileCheck %s --check-prefixes=ATOMICS
; RUN: llc < %s --mtriple=wasm32 -mattr=+atomics,+relaxed-atomics | FileCheck %s --check-prefixes=RELAXED32
; RUN: llc < %s --mtriple=wasm64 -mattr=+atomics,+relaxed-atomics | FileCheck %s --check-prefixes=RELAXED64
; A multithread fence is lowered to an atomic.fence instruction.
; CHECK-LABEL: multithread_fence:
; CHECK: atomic.fence
; NOATOMIC-NOT: i32.atomic.rmw.or
define void @multithread_fence() {
fence seq_cst
define void @fence_acquire() {
; NO-ATOMICS-LABEL: fence_acquire:
; NO-ATOMICS: .functype fence_acquire () -> ()
; NO-ATOMICS-NEXT: # %bb.0:
; NO-ATOMICS-NEXT: # fallthrough-return
;
; ATOMICS-LABEL: fence_acquire:
; ATOMICS: .functype fence_acquire () -> ()
; ATOMICS-NEXT: # %bb.0:
; ATOMICS-NEXT: atomic.fence
; ATOMICS-NEXT: # fallthrough-return
;
; RELAXED32-LABEL: fence_acquire:
; RELAXED32: .functype fence_acquire () -> ()
; RELAXED32-NEXT: # %bb.0:
; RELAXED32-NEXT: atomic.fence acqrel
; RELAXED32-NEXT: # fallthrough-return
;
; RELAXED64-LABEL: fence_acquire:
; RELAXED64: .functype fence_acquire () -> ()
; RELAXED64-NEXT: # %bb.0:
; RELAXED64-NEXT: atomic.fence acqrel
; RELAXED64-NEXT: # fallthrough-return
fence acquire
ret void
}
; Fences with weaker memory orderings than seq_cst should be treated the same
; because atomic memory access in wasm are sequentially consistent.
; CHECK-LABEL: multithread_weak_fence:
; CHECK: atomic.fence
; CHECK-NEXT: atomic.fence
; CHECK-NEXT: atomic.fence
define void @multithread_weak_fence() {
fence acquire
define void @fence_release() {
; NO-ATOMICS-LABEL: fence_release:
; NO-ATOMICS: .functype fence_release () -> ()
; NO-ATOMICS-NEXT: # %bb.0:
; NO-ATOMICS-NEXT: # fallthrough-return
;
; ATOMICS-LABEL: fence_release:
; ATOMICS: .functype fence_release () -> ()
; ATOMICS-NEXT: # %bb.0:
; ATOMICS-NEXT: atomic.fence
; ATOMICS-NEXT: # fallthrough-return
;
; RELAXED32-LABEL: fence_release:
; RELAXED32: .functype fence_release () -> ()
; RELAXED32-NEXT: # %bb.0:
; RELAXED32-NEXT: atomic.fence acqrel
; RELAXED32-NEXT: # fallthrough-return
;
; RELAXED64-LABEL: fence_release:
; RELAXED64: .functype fence_release () -> ()
; RELAXED64-NEXT: # %bb.0:
; RELAXED64-NEXT: atomic.fence acqrel
; RELAXED64-NEXT: # fallthrough-return
fence release
ret void
}
define void @fence_acq_rel() {
; NO-ATOMICS-LABEL: fence_acq_rel:
; NO-ATOMICS: .functype fence_acq_rel () -> ()
; NO-ATOMICS-NEXT: # %bb.0:
; NO-ATOMICS-NEXT: # fallthrough-return
;
; ATOMICS-LABEL: fence_acq_rel:
; ATOMICS: .functype fence_acq_rel () -> ()
; ATOMICS-NEXT: # %bb.0:
; ATOMICS-NEXT: atomic.fence
; ATOMICS-NEXT: # fallthrough-return
;
; RELAXED32-LABEL: fence_acq_rel:
; RELAXED32: .functype fence_acq_rel () -> ()
; RELAXED32-NEXT: # %bb.0:
; RELAXED32-NEXT: atomic.fence acqrel
; RELAXED32-NEXT: # fallthrough-return
;
; RELAXED64-LABEL: fence_acq_rel:
; RELAXED64: .functype fence_acq_rel () -> ()
; RELAXED64-NEXT: # %bb.0:
; RELAXED64-NEXT: atomic.fence acqrel
; RELAXED64-NEXT: # fallthrough-return
fence acq_rel
ret void
}
define void @fence_seq_cst() {
; NO-ATOMICS-LABEL: fence_seq_cst:
; NO-ATOMICS: .functype fence_seq_cst () -> ()
; NO-ATOMICS-NEXT: # %bb.0:
; NO-ATOMICS-NEXT: # fallthrough-return
;
; ATOMICS-LABEL: fence_seq_cst:
; ATOMICS: .functype fence_seq_cst () -> ()
; ATOMICS-NEXT: # %bb.0:
; ATOMICS-NEXT: atomic.fence
; ATOMICS-NEXT: # fallthrough-return
;
; RELAXED32-LABEL: fence_seq_cst:
; RELAXED32: .functype fence_seq_cst () -> ()
; RELAXED32-NEXT: # %bb.0:
; RELAXED32-NEXT: atomic.fence seqcst
; RELAXED32-NEXT: # fallthrough-return
;
; RELAXED64-LABEL: fence_seq_cst:
; RELAXED64: .functype fence_seq_cst () -> ()
; RELAXED64-NEXT: # %bb.0:
; RELAXED64-NEXT: atomic.fence seqcst
; RELAXED64-NEXT: # fallthrough-return
fence seq_cst
ret void
}
; A singlethread fence becomes compiler_fence instruction, a pseudo instruction
; that acts as a compiler barrier. The barrier should not be emitted to .s file.
; CHECK-LABEL: singlethread_fence:
; CHECK-NOT: compiler_fence
; CHECK-NOT: atomic_fence
define void @singlethread_fence() {
; NO-ATOMICS-LABEL: singlethread_fence:
; NO-ATOMICS: .functype singlethread_fence () -> ()
; NO-ATOMICS-NEXT: # %bb.0:
; NO-ATOMICS-NEXT: # fallthrough-return
;
; ATOMICS-LABEL: singlethread_fence:
; ATOMICS: .functype singlethread_fence () -> ()
; ATOMICS-NEXT: # %bb.0:
; ATOMICS-NEXT: # fallthrough-return
;
; RELAXED32-LABEL: singlethread_fence:
; RELAXED32: .functype singlethread_fence () -> ()
; RELAXED32-NEXT: # %bb.0:
; RELAXED32-NEXT: # fallthrough-return
;
; RELAXED64-LABEL: singlethread_fence:
; RELAXED64: .functype singlethread_fence () -> ()
; RELAXED64-NEXT: # %bb.0:
; RELAXED64-NEXT: # fallthrough-return
fence syncscope("singlethread") seq_cst
fence syncscope("singlethread") acquire
fence syncscope("singlethread") release

File diff suppressed because it is too large Load diff

View file

@ -41,6 +41,26 @@ define i32 @load_i32_with_folded_gep_offset(ptr %p) {
ret i32 %t
}
; Same for nusw.
; CHECK-LABEL: load_i32_with_folded_gep_offset_nusw:
; CHECK: i32.atomic.load $push0=, 24($0){{$}}
define i32 @load_i32_with_folded_gep_offset_nusw(ptr %p) {
%s = getelementptr nusw i32, ptr %p, i32 6
%t = load atomic i32, ptr %s seq_cst, align 4
ret i32 %t
}
; For nuw we don't need the offset to be positive.
; CHECK-LABEL: load_i32_with_folded_gep_offset_nuw:
; CHECK: i32.atomic.load $push0=, -24($0){{$}}
define i32 @load_i32_with_folded_gep_offset_nuw(ptr %p) {
%s = getelementptr nuw i32, ptr %p, i32 -6
%t = load atomic i32, ptr %s seq_cst, align 4
ret i32 %t
}
; We can't fold a negative offset though, even with an inbounds gep.
; CHECK-LABEL: load_i32_with_unfolded_gep_negative_offset:
@ -99,8 +119,19 @@ define i32 @load_i32_from_global_address() {
ret i32 %t
}
define i32 @load_i32_global_with_folded_gep_offset_nonconst_nuw(i32 %idx) {
; CHECK-LABEL: load_i32_global_with_folded_gep_offset_nonconst_nuw:
; CHECK: i32.const $push0=, 2
; CHECK: i32.shl $push1=, $0, $pop0
; CHECK: i32.atomic.load $push2=, gv($pop1)
%s = getelementptr nuw i32, ptr @gv, i32 %idx
%t = load atomic i32, ptr %s seq_cst, align 4
ret i32 %t
}
;===----------------------------------------------------------------------------
; Atomic loads: 64-bit
;===----------------------------------------------------------------------------
; Basic load.