As we all know, the atomic primitives and standards mostly comes from the C++ 11 atomic primitives standards and definitions. But in C++ 26, even C++ deprecated APIs like atomic_load/store and moves to smarter type abstractions like std::atomic<std::shared_ptr>. Many programming language are moving away from the raw pointer style atomic primitives to a more type safe one. (C++ P2869R4)
So what I think is a key question now is whether rust shall also introduce a new set of smarter and safer atomic types and primitives. Like introducing something like Atomic<T>.
Application layer rust code mostly use abstractions like Arc<T>Box<T> and so on, so this will not effect them much; but when dealing with lockless asynchronous designs, surely Atomicptr<T> (with unsafe and raw pointers) is a must. So in this sense, it is necessary for us to introduce some more well abstracted atomic primitives as far as I personally think.
Also, there all a great deal of crates providing similar features out there already, such as arc-swap, but atomic primitives, well, seems to me as a thing that shall be provided by the standard library to avoid to much friction and code quality issue.
But there are several key difficulties out there in order to implement this. Atomic types will mostly need to be Unsized here, but as far as I know no CPU architecture supports atomic swapping an dyn trait...
So this shall be consider as involving careful language design works, not only a libs issue.
But anyway, supporting such an atomic type will surely improve the safety of unsafe rust in system programming, and with a powerful compiler analyse infrastructure, we can do much more checks at compile time using const.
The main thing on my mind is MaybeUninit variants of the atomics that Rust already has. Though... IIRC compare-and-swap would need something like freeze to be useful, so MaybeUninit atomics are a matter of opsem and very low-level raw-pointer primitives, not abstractions, so that's probably not what you're thinking about.
That said... I think supporting MaybeUninit atomics is a prerequisite of supporting a maximally-general Atomic<T> where size_of::<T>() is sufficiently small, since a general T can have uninit padding bytes.
All atomic types except for std::atomic_flag may be implemented using mutexes or other locking operations, rather than using the lock-free atomic CPU instructions. Atomic types are also allowed to be sometimes lock-free, e.g. if only aligned memory accesses are naturally atomic on a given architecture, misaligned objects of the same type have to use locks.
Although this satisfies the notion of "atomic", it's not what most people think about when talking about "atomic types". As such I find extending Atomic<T> to all types T to a bit too footgunny for the Rust stdlib.
Since you mentioned arc-swap, that's not a basic atomic primitive, it's a fully fledged concurrent memory reclamation algorithm, possibly not the best either.
Oh thanks I see it now, and it is blocked by the Atomic 128 issue.
Yes so in this sense we are pretty close and the issues are smaller (because Atomic 128 has been soft blocking it for so long I even forget it in the first place...), but as far as I see the generic atomic RFC for now just support atomics under AtomicPrimitive, do we have any future plans to upgrade it for supporting more general fat pointers or structures? (That might involve some really hard memory semantic issues... And maybe we cannot support it mathematically speaking, but I am not sure)
One of my main visions for Rust is "safe Rust is powerful enough that you don't need to write unsafe Rust", with the compiler checking memory safety even if you're doing weird low-level things. From this point of view, the most important sorts of atomic are atomic references and (in particular) atomic enums (you can get a huge amount of power by filling enums full of ZSTs that serve as proofs of things).
One problem is atomic ordering (you need to make sure that if you use an atomic to communicate a value from one thread to another, the receiving thread can see in memory all the values that the sending thread wrote there), but I think acquire/release is sufficient to handle all the relevant cases.
Atomic enums are quite hard to implement, though, primarily because you need to be able to make them small enough to fit in an atomic (and to prove that you have done so), and that can require tricks like packing flag bits into alignment niches and the like (which contradict how Rust enums normally work). You might need some sort of #[repr(bit_packed)] on the enum to make it work.
The main problem with atomic enums is that they commonly include padding bytes and thus require MaybeUninit atomics, like stated above. Other than that (which is solvable but not great), it's possible to make a library for them (and someone probably did).
I guess we have different ideas of how atomic enums would likely be used – with the sort of enums I'm thinking of, it's a struggle to fit all the value bits into the enum, and fitting in padding too would make things even more difficult.
It strikes me that a #[repr(bit_packed)] would probably want to ensure that each possible value had only one representation anyway (to be able to fit more possibilities in) and that would automatically solve the padding problem. (My vision for it goes something like this: "there are 0x1FFF_FFFF_FFFF_FFFE possible values for an &u64, so let's give them the representations 0..0x0x1FFF_FFFF_FFFF_FFFE, then there are 0x100_0000 possible values for a (u8, u16), so let's give them the representations 0x0x1FFF_FFFF_FFFF_FFFE..0x2000_0000_00FF_FFFE…". Having more padding bytes in the original values just means that you can pack them more tightly into possible values for the enum.)
For std::shared_ptr specifically, the history is as you say. C++11 originally exposed atomic operations on raw std::shared_ptr<T> * pointers; then C++20 added a safe wrapper, std::atomic<std::shared_ptr<T>>; then C++26 removed the old API.
For every other atomic type though (integers, raw pointers, etc.), a similar evolution occurred but much further in the past. Prior to C++11, platform-specific APIs provided atomic operations on raw T * or volatile T * pointers. When C++11 standardized atomics, it already provided the safe wrapper type, std::atomic<T>. In fact, C++11 only provided the safe wrapper type, and there was no standardized way to perform atomic operations on non-atomic pointers until C++20 added std::atomic_ref.
This thinking is the narrow vision of “atomic,” enshrined by the C++ implementation. DBs and Wikipedia have a very different view of that word. That is based on what you get, not implementation details. Availability of other Rust features also does not hinge on “is done in a single CPU instruction.”
Now, whether Atomic<T> is feasible in a way that’s interoperable with the existing atomics, is another question. If it is, there should be a warning in the doc that operations on non-CPU-based types can be much slower.
That seems like a terrible idea. Degrading to a slower mode of operation is worse than not compiling at all in the domain I work in (hard realtime machine/vehicle control) and so that would be yet another thing to worry about and need Lint's for.
What would make more sense is to have a separate type for it. Which we already do, in the form of Mutex. If it is about the API of Atomic being preferable, you could add those on top with an extension trait.
That is not correct, some atomic operations may use more than one CPU instruction, in particular on 32 bit ARM using LL/SC.
Shouldn't that be possible for all small types whose memory representation can be canonicalized, the bits can be safely copied and byte-equality on the canonicalized version is meaningful?[1]canonical would as far as I can tell only need to set padding and uninitialized bits to 0 (or return a copy). [2]
Another option (less flexible) could be an auto-generated trait that returns a bitmask of all padding bits (assuming the are allowed to be read as long as they are discarded through the mask.
Could be explicitly defined as NOT using PartialEq/Eq, floats for example would probably have NaN==NaN on the canonicalized version. ↩︎
Not fully sure about niches, though those should not be used by an outer type if we have a &T already. ↩︎
To the first paragraph: that seems reasonable. Could probably make a library that does this. Much like bytemuck or zerocopy, you can soundly do a lot of unsafe shenanigans with a sufficient quantity of traits and trait implementations.
Unfortunately, if this library wants to support putting (say) two pointers in an atomic, then it’d probably need to use exposed provenance (other solutions, like relying on libc or inline asm, seem worse). So Miri would be less helpful for users of the library.
To the second paragraph: AFAIK, doing bitwise operations on uninit bytes is not a capability exposed by Rust. (At the very least, MaybeUninit<u8> doesn’t have them.) Not sure if that’s for LLVM reasons. Masking out entire uninit bytes by simply not reading and copying them would be easier.
I'm not sure I understand your point since I was blaming C++ for being too "wide" under that point of view.
That said, if you think if naming the current types "atomic" is wrong, we should consider change their name rather than changing what they do. Performance is a defining characteristic of them, and a wrong name should not change that.