There have been several requests to add additional atomic types to Rust (here and here). One of the main drivers for this change is that some lock-free algorithms requires a double-word compare-and-swap, which is currently not possible in Rust since the largest atomic type is only one word long.
Regarding platform support: after a quick survey of the LLVM and Clang code, architectures can be classified into 3 categories:
- The architecture does not support any form of atomics (mainly microcontroller architectures).
- The architecture supports all atomic operations for integers from i8 to iN (where N is the architecture word/pointer size).
- The architecture supports all atomic operations for integers from i8 to i(N*2).
There are several ways in which this support can be implemented. This pre-RFC is intended to give an overview of each approach and elicit feedback from the community for which approach should be considered for an RFC. New ideas are o course welcome, and I will add them to this post.
Atomic pair types
#[cfg(target_atomic_pair)]
struct AtomicPtrPair<T, U> {}
#[cfg(target_atomic_pair)]
struct AtomicUsizePair {}
#[cfg(target_atomic_pair)]
struct AtomicIsizePair {}
These types would work like existing atomic types except that instead of working with single value they would work with tuple pairs. For example AtomicPtrPair<T, U>
would allow atomically swapping a (*mut T, *mut U)
.
A #[cfg]
attribute is used to only make these types available on architectures that support them. User code will be able to use this same attribute to select between code using atomic pairs and code using other synchronization (such as a mutex).
The downside of this approach is that sometimes you want to have a (*mut T, usize)
, and adding new types for each combination is impractical.
More atomic integer types
struct AtomicI8 {}
struct AtomicU8 {}
struct AtomicI16 {}
struct AtomicU16 {}
struct AtomicI32 {}
struct AtomicU32 {}
#[cfg(target_atomic_64)]
struct AtomicI64 {}
#[cfg(target_atomic_64)]
struct AtomicU64 {}
#[cfg(target_atomic_128)]
struct AtomicI128 {}
#[cfg(target_atomic_128)]
struct AtomicU128 {}
These types are fairly straightforward extensions of the existing AtomicIsize
and AtomicUsize
types, and support the same methods. Structs can be used with these atomics by transmuting them to an integer type before storing them in an atomic variable.
As with the previous approach, a #[cfg]
attribute is used to conditionally expose the 64-bit and 128-bit atomic types. These are not required for 8, 16 and 32-bit atomic types because these are always available on all architectures that support atomics.
One downside is that AtomicI128
and AtomicU128
would probably require adding i128
and u128
to Rust, which brings another set of issues:
- 128-bit integers are only available on 64-bit platforms, not 32-bit ones.
- Adding
i128
andu128
to the prelude would probably be a breaking change.
Generic atomic type
struct Atomic<T> {}
This would allow any type to be made atomic, similarly to the C++ std::atomic<T>
type. The given type T
is padded so that its size is a power of two and it is aligned to its size. If a native atomic type of the required size is not available, the atomic operations are transparently translated into a runtime library call which emulates the atomic operation using mutexes.
While this provides a great amount of flexibility, there are two major downsides. The first is that there is no way to implement such a type with the language features currently available (manipulating size and alignment in generic types). The second is that automatically translating atomic operations into runtime library calls can result in surprising behavior because the emulated operation uses a lock.
Generic atomic type with size restrictions
struct Atomic<T>
where sizeof(T) < 64/128
{}
This is the same as the previous approach without the option to fall back to a library call. Instead any type that does not fit in a native atomic type will cause a compile-time error. Unfortunately this still doesn’t resolve the first issue, which is that implementing such a type is not possible with the current set of language features. It also becomes harder for a user to know whether his code will compile on any given architecture.
Generic atomic type based on a trait
trait AtomicOps {
fn compare_and_swap(self: *mut Self, current: Self, new: Self, order: Ordering) -> Self;
fn swap(self: *mut Self, val: Self, order: Ordering) -> Self;
fn load(self: *mut Self, order: Ordering) -> Self;
fn store(self: *mut Self, val: Self, order: Ordering);
}
trait AtomicIntOps: AtomicOps { // Implemented for integers
fn fetch_add(self: *mut Self, val: Self, order: Ordering) -> Self;
fn fetch_sub(self: *mut Self, val: Self, order: Ordering) -> Self;
}
trait AtomicLogicalOps: AtomicOps { // Implemented for integers and bool
fn fetch_and(self: *mut Self, val: Self, order: Ordering) -> Self;
fn fetch_nand(self: *mut Self, val: Self, order: Ordering) -> Self;
fn fetch_or(self: *mut Self, val: Self, order: Ordering) -> Self;
fn fetch_xor(self: *mut Self, val: Self, order: Ordering) -> Self;
}
struct Atomic<T: AtomicOps> {}
impl Atomic<T> where T: AtomicOps {}
impl Atomic<T> where T: AtomicIntOps {}
impl Atomic<T> where T: AtomicLogicalOps {}
impl AtomicOps for bool {}
impl AtomicOps for isize {}
impl AtomicOps for *const T {}
impl AtomicOps for *mut T {}
impl AtomicOps for i8 {}
impl AtomicOps for i16 {}
impl AtomicOps for i32 {}
#[cfg(target_atomic_64)]
impl AtomicOps for i64 {}
#[cfg(target_atomic_128)]
impl AtomicOps for i128 {}
// And now the types in `std` can be recovered as:
type AtomicBool = Atomic<bool>;
type AtomicIsize = Atomic<isize>;
type AtomicUsize = Atomic<usize>;
type AtomicPtr<T> = Atomic<*mut T>;
This approach is a mix between adding more atomic integer types and a generic atomic type. This uses a generic Atomic<T>
type which only supports a predefined set of T
types for which atomic operations are defined.
A #[cfg]
attribute is still needed to restrict support for 64-bit and 128-bit atomics on platforms that don’t support them, and a i128
type is needed for 128-bit atomics.