[RFC] Add IR support for non-fetch atomicrmw (C++26 P3111)

As part of implementing P3111 Atomic reductions in libc++ (#186935), i’ve added support for these operations to LLVM IR in a local branch. How these are exposed in the IR significantly influences how the different backends override them and how clang builtins are lowered. Therefore I’m opening this RFC to gain feedback on the preferred way to model these in the IR.

Background

Store atomics (C++26 void std::atomic<int>::store_add(int v, memory_order);) are atomic read-modify-write operations that do not return the value read (they return void), and whose read operation is excluded from building acquire patterns with acquire fences. They are therefore subtly different from fetch atomics (int std::atomic<int>::fetch_add(int v, memory_order) in terms of semantics, optimizations, memory orders supported (e.g. acquire is not supported), etc.

Multiple LLVM backends support them (NVPTX red, Aarch64 STADD, ppc64le AMOs, probably others…).

In particular, optimizing a fetch atomic operation into a store atomic operation is - in general - unsound, i.e., programs that need to generate these reliably, need an opt-in mechanism. On the other hand, store atomic operations are always correct to lower to fetch atomic operations, and the AtomicExpand pass can expand them to fetch atomics to provide generic support for them for all backends that choose to not override them.

IR exposure

My preference is to add a storermw (or similarly named) option to atomicrmw.

  • PRO: avoids adding a new IR instruction, can tweak pre-existing optimization passes, ensures the same operations are supported with/without the option, can use syncscope and memory ordering fields, etc.
  • CON: bolts subtly different semantics on top of atomicrmw

The other viable alternative (which i’ve implemented locally) is to add a new instruction, e.g., atomicred that keeps these operations separate from atomicrmw. The main CONs w.r.t an option on atomicrmw is: higher impl/maintenance effort, higher effort to support same operations in both. I think adding a modifier to atomicrmw has better ROI.

From the point-of-view of frontend and backends, there is not a huge difference between these two options in my opinion.

As part of this effort I tried a few other alternatives, that turned out to not be viable. The main one was exposing these new operations via LLVM intrinsics (llvm.atomicred...). This approach avoids adding/modifying pre-existing instructions, but since we don’t support memory orders, syncscope, … arguments on intrinsics, it turned out to be a pretty painful change (its hard to map synscope to integers in a frontend, would need to use i32 for memory orders, …).

Does anyone have any feedback on the best way to proceed?
Or in the absence of feedback, does anyone object to adding a new option to atomicrmw?

Thank you
Gonzalo

I think it’s important that a “non-fetch” instruction has a void return value; your description doesn’t clarify whether the “storermw” modifier implies this. Otherwise, I don’t have a strong preference for modified RMW vs. a separate instruction vs. an intrinsic; whatever is easier is probably fine.

Is atomicrmw xchg ptr %p, i32 %x monotonic storermw, align 4 just a store, or does it have slightly different semantics?

Do we need a storermw variant of cmpxchg? The paper implies such an operation exists on Arm, although the Arm manual doesn’t seem to mention it, and I’m not sure how you’d use that operation.

(On a side-note, I’m having trouble following the Arm manual’s description of stadd; multiple places in the informal text note that the destination register affects the atomic semantics, but nothing in the pseudocode or the or formal memory model mentions it.)

1 Like

I agree. I have not fully implemented the storermw modifier yet, so I don’t know how hard would it be for it to change the return type from atomicrmw to void. My current implementation of the storermw modifier returns undef, which is something I dislike. My implementation that adds a separate instruction does return void and does not have this issue.
If I fail to change the return type of atomicrmw to void depending on whether storermw is used, would it be fair to say that you’d prefer a separate instruction that returns void instead?

The atomicrmw xchg operation is not supported if storermw is used.
In my implementation, the storermw modifier is not currently supported for cmpxchg.
Rationale is that C++26 decided to not standardize a store-atomic version of cmpxchg due to lack of motivating use case for now. If we find such an use case, we can always extend C++ and the LLVM IR later.

Arm load atomics (LDADD) become store atomics when the destination operand is the zero-register (and STADD shares the same machine encoding with LDADD with a zero register destination operand). This information is not easy to find in the current Arm Architecture Reference Manual. The LDADD instruction has a Note for its acquire versions, and “C3.2.12 Atomic instructions” states the following which applies to the remaining versions:

The ST instructions, and LD instructions where the destination register is WZR or XZR, are not regarded as doing a read for the purpose of a DMB LD barrier.

Why is this important? I think it would be fine to spec atomicrmw storermw to return poison.

If we do want a void return type, then I think it would be best to add a new instruction rather than a modifier.

@gonzalobg As you already implemented the separate instruction, can you maybe share a draft PR, so we can get an idea of how large the impact is?

It works in the sense that you can have a self-consistent implementation. It just seems confusing for anyone reading the IR to see a definition of a value which is actually just guaranteed poison.

It sounds like it is always poison-returning semantically early on, but it could be CSE with or lowered to a regular atomicrmw simply by ignoring (dropping) the metadata, which might be fairly consistent with existing IR design.

See [IR] Add storermw instruction by gonzalobg · Pull Request #207048 · llvm/llvm-project · GitHub .

FWIW I agree with this is confusing, I prefer the approach of having a separate instruction.

Just as a quick question (without having read the spec yet), couldn’t we just use an atomicrmw where the result isn’t used to model this?

No, this instruction has different effects on the memory model than atomicrmw – it’s not just an atomicrmw with an unused result.

For that reason, a distinct IR instruction seems like the best choice.

2 Likes

So the PR should be ready for review.