-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a generic Atomic<T> type #1505
Conversation
Dup of #1477 |
It's not exactly a duplicate. The proposed approach is completely different. #1477 only talks about changing unstable intrinsics while this RFC proposes adding a new stable atomic API. |
Right, that's true. It might be worth mentioning #1477? Nonetheless, I 👍 this, since this will make everything much easier. |
For people coming from C++ (which is Rust's target audience), there needs to be an equivalent of |
Thanks for the RFC @Amanieu! I've also wanted this from time to time, although there's a number of questions I've had that I unfortunately haven't been able to answer in the past. I'm curious as to your thoughts on:
Curious to hear what you think about these! |
I feel like the bitwise comparison is more significant than what's written here (which is differing struct padding). In particular, Rust generally avoids thinking about object identity, instead focusing on actual values/semantic comparison. Thus, let atomic = Atomic::new(&1);
let old = &1;
assert!(atomic.load() == old); // passes
atomic.compare_exchange(old, &2); // fails (The orderings don't matter, and this may not compile as written due to lifetimes, and optimisations may use the same pointer for the two I idly wonder if it might be better to have let atomic = Atomic::new(BitwiseCmp(&1));
let old = BitwiseCmp(&1);
atomic.compare_exchange(old, BitwiseCmp(&2)); I'm on the fence about this:
Also, some thoughts I had while writing this:
(These are both addressed by having a bound more specific than Lastly, diving into pedant-land, some types have undefined values (bit patterns that should never occur, or are only valid sometimes) such as references and |
Do we need to guarantee that
One might want to be working with some "random" type, e.g. a state-machine Also, for double-word atomics, maybe one has |
|
Hmm actually it seems that we can't always guarantee that values up to the pointer size are guaranteed to be atomic. This is only an issue for platforms that don't support atomic operations natively, such as ARMv5. On ARMv5, But my point here is, what should we do about architectures like ARMv5 that don't support native atomic operations at all? Should we disallow atomics entirely? Should we silently fall back to lock-based implementations? |
Expanding a little on @Amanieu's notes about locks above: In considering this, it's probably important to understand how C++ handles a similar question in it's C++ considers This separation is done to allow users to write code that uses For a generic atomic to make sense in rust (much like in C++), we need it to work for all types. Platform based restrictions here would greatly hamper usability of the standard library. It might be useful to have lockfree-only variants for use in problematic areas as noted by @Amanieu above (interrupt handlers, signal handlers) but I'm not sure rust is very well specified in those areas to begin with. To take the case of signal handlers, POSIX has a different (and incredibly more restrictive) set of rules there than it does for separate threads. At some point we'll need to handle them, but right now we lack any rules about them (AFAIKT), so I don't think it would be a good idea to allow them to restrict |
Ah sorry for being a little slow to respond, but thanks for the responses! I hadn't actually considered implementing I would personally not want the standard library to back atomics with mutexes, it's too sweet of a name to silently switch to something so heavyweight behind the scenes (easy to forget). I also agree that threading this through the trait system is probably not possible, but it's also a serious drawback in my mind in the sense that I would personally consider it a non-starter. I guess it's true, yeah, though that |
@alexcrichton I've added a note about this to the RFC. Note that this emulation is done transparently by LLVM, so there is nothing we need to do on the Rust side to support it.
I only see two approaches for supporting generic atomic types:
Other approaches (in this thread) only support implementing atomic operations for specific types, such as |
I can't agree with this. "Atomic" has a semantic meaning that doesn't mean "native CPU instructions for atomic operations." Those just happen to be used if available. There's also a solid argument to be made that if C++'s |
@alexcrichton Wouldn't Really, though, I think the semantics for the identifiers |
I think it would be more of a waste to make it equivalent to Edit: it's also good to note that if we'll need something that can fallback to allow people to write cross platform code that is able to take advantage of platform capabilities ( |
The JVM atomic types (
There's quite a bit of precedent behind making "atomic" mean "lock-free if possible, but falls back to locks if needed." |
I'm personally very weirded out if we ever pull in a huge amount of support for compiler-rt. That's a dependency pulled into all rust projects in existence so we need to control it and understand it as much as possible. I would very much prefer to write any support we need in Rust itself so we understand the pain we're going through to do that. To me having an array of global locks is distasteful enough that we shouldn't export it as a cross-platform interface in the standard library, for example. It's basically just my own personal opinion that atomics should map to instructions, not mutexes. I haven't done much with atomics in C++, but I would personally avoid them in situations where they fell back to mutexes. To me at least it seems like a cross-platform |
Fair point, but using compiler-rt is entirely orthogonal to the question "should Rust have an Like you, I too would prefer a Rust solution.
That seems like a fine personal/per-project decision, but not a great "global" one. Lots of people want a type that means "atomic access that's as fast as possible, but always correct" which is what It makes complete sense that some projects want something else, which would be direct use of a
This runs counter to the conclusions of the C++ standards committee and the engineers who work on the JVM. There is a direct need for a type like this (as evidenced by C++, Java and this RFC), and Rust should have it. There's a stark difference between "I don't need this" and "nobody should ever need this." Like I mentioned above, having an |
The global mutex thing feels very strongly like something that should live in a library to me. In fact, I think it could be done in regular Rust code today. Each atomic method would at "runtime" switch on the return value of size_of, transmute to the appropriate pointer type, and do the atomic operation on that; LLVM would remove the unnecessary branches. For unsupported sizes, you'd fall back to mutexes. (You’d have to bake in assumptions about what sizes of integer can be subject to atomic operations, but there are many ways Rust could convey that static information to code in the future.) The case this approach falls down in is if you don't want the global mutexes and thus want an unsupported size to produce some sort of error at trans time. I guess you could hack it with |
I feel like having |
I strongly object. While every "atomic" operation that falls back to the mutex now involves multiple hardware operations, it's still very definitely atomic in the sense of the word as intermediate states are not observable from safe rust. Similarly, you could argue that x86's atomic instructions aren't really atomic because they may cause bus locking. The alternative of erroring out may look like a speedbump against accidental mutexes, but I'd argue that this works poorly in practice: Nowadays, with x86 everywhere, Rust projects are rarely going to be developed on multiple platforms, so many developers probably wouldn't hit the error that their code is incompatible with e.g. ARM. This introduces a huge portability hazard: You'd be unable to use crates unless the author specifically thought of your architecture (or you're lucky, of course). It essentially blows up the ecosystem into many tiny fragments. Falling back to a mutex here makes things slower, but it works. A potential performance footgun is an easy sacrifice to make when it allows us to maintain something as big as cross-platform compatibility - especially as I'd expect atomic variables to mostly be used by experienced programmers anyways who understand the pitfalls. Now when a crate's developer does care about platforms where the fallback would occur, they can just branch on tl;dr Mutex fallback is unfortunate but I consider it unavoidable. |
Is there any reason we couldn't put the |
The downside of including a Also another reason is that, in C++, an atomic can sometimes be lock free if it depends on certain runtime features from the kernel (for example on ARMv5 the kernel provides a callable |
Ah sorry I think I'm not communicating my preference here clearly. The precedence of other languages doesn't mean much to me in terms of what the API of the standard library should look like, but rather what it should have. I agree that concrete types like I'm all for providing primitives, but having a heavyweight runtime implementation with global mutexes or out of bounds reads to me sounds like a library implementation detail, not something exported as an atomic primitive (as those implementations indeed aren't primitive).
To me this is true, but no more so than the Unix/Windows support in the standard library. We do a "best effort" to provide cross platform APIs, but whenever you need something specific we provide the ability, it just needs to be forcibly opted into. For example maybe these types are only available in |
I agree strongly with @alexcrichton here - a heavyweight fallback seems distinctly unrusty to me. It masks the performance implications of atomic operations, doesn't map clearly to a system primitive, and is likely to have odd differences in behavior. I also feel very nervous about adding the possibility for a compile failure at monomorphization time! Checked generics (generics that are checked at the definition site not the usage site, like C++ templates) are one of rust's best features in my opinion. Violating this property is a very slippery slope to go down, and we've resisted adding other more useful features, like type-level numbers (which would allow implementing this whole scheme outside of std), without a solution to this problem. I also agree with @alexcrichton that we should just provide zero-cost architecture specific atomics in architecture specific modules and allow library authors to play with heavyweight fallbacks. EDIT: Just to add an example of where this sort of magic would be bad for users, imagine you are writing a signal handler that access a global atomic. If we where to provide a mutex fallback on some platforms, this code could deadlock on those architectures, which is extremely unexpected behavior for an atomic primitive. EDIT2: I would also like to echo @alexcrichton again in saying that the availability of this type with similar semantics in other languages is not a convincing argument on its own - there's plenty of stuff in both Java and C++ std libraries that are not at all appropriate for rust std. |
@alexcrichton so I take it you would prefer something similar to this: #[cfg(target_has_atomic = "8")]
struct AtomicI8 {}
#[cfg(target_has_atomic = "8")]
struct AtomicU8 {}
#[cfg(target_has_atomic = "16")]
struct AtomicI16 {}
#[cfg(target_has_atomic = "16")]
struct AtomicU16 {}
#[cfg(target_has_atomic = "32")]
struct AtomicI32 {}
#[cfg(target_has_atomic = "32")]
struct AtomicU32 {}
#[cfg(target_has_atomic = "64")]
struct AtomicI64 {}
#[cfg(target_has_atomic = "64")]
struct AtomicU64 {}
#[cfg(target_has_atomic = "128")]
struct AtomicI128 {}
#[cfg(target_has_atomic = "128")]
struct AtomicU128 {} Note that the existing atomic types are already dependent on the target architecture supporting atomic operations. It just so happens that all currently supported targets (tier 1, 2 and 3) all support atomic operations natively. |
Yes, this is why
Which is why there have been other arguments. On the other side of this, we should be careful not to ignore why other languages chose to do things in some manner. We've got a lot of resources we can take advantage of by looking at other language designs. |
@alexcrichton Note that my worries are specifically about a generic atomic type. With the approach you have in mind where concrete types are available under architecture-specific modules, I do agree that the issue is much less severe as programmers would then explicitly opt in to cross-platform incompatibility. |
Certainly! Here we're talking about the standard library, however, rather than just any old library. The standard library, in my opinion, is where you go for types that have clear and concise implementations and semantics. For example a An I certainly agree that there are many users who "just want the fastest thing" or "just want an
Along those lines, yeah! I'd specifically be thinking of having architecture-specific modules (similar to std::arch::x86::{AtomicU8, ..., AtomicU32};
std::arch::x86_64::{AtomicU8, ..., AtomicU64};
std::arch::arm::{AtomicU32}; And we may be able to extend that to so we have a std::arch::pointer_width_32::AtomicU32;
std::arch::pointer_width_64::AtomicU64;
This is certainly quite interesting to me! This is part of where I feel like our story on other architectures is a little lacking. I feel like we don't have a great idea what's going on here. Do you have some specifics we could dig into though? On one hand the ship has sailed here (the Some questions I might have are:
I'm getting more and more uncomfortable about what operations are silently translated to calls into compiler-rt. We basically have no idea when that library is used or what we need from it. Not to mention it's a huge PITA to build almost everywhere... |
Alternately, I don't see why it couldn't be implemented through the trait system: Have magic traits Implementation-wise, this would require doing type memory layout much earlier than it's currently done, which probably makes it difficult, but I think that should be done anyway, for the sake of |
@comex this doesn't solve the problem of types being generic over |
It does, for some definition of "declaration time". The
*actually, it might have to be something like Edit: Actually, that basically makes no sense at all. So |
Here is a quick survey of atomic support for all existing Clang/LLVM targets:
Note from the list above that there is quite a bit of variation even within a single architecture, so it doesn't really make sense to have a separate set of atomic operations per architecture.
Not necessarily. We could argue that This is similar to the proposal for float-free libcore which involves removing
While I agree that a mutex-based implementation may be an issue, the code to use out-of-bounds reads to simulate smaller atomic sizes that natively support is implemented directly in LLVM backends. LLVM will generate the necessary masking and compare-exchange loop inline, without the need to call into compiler-rt.
All of the platforms we support have pointer-sized atomic operations. The oldest ARM platform that is supported is
The decision to fall back to compiler-rt is done in the LLVM backend based on whether the target architecture has native support for atomics of the required size. If a native operation is not available then it will automatically fall back to a call to compiler-rt. |
@Amanieu thanks for the impressive investigation! I think your conclusion about avoiding architecture-specific modules is correct, it seems like that wouldn't benefit much here as there's lots of variance withing the architecture itself. I wonder, however, if we could possibly use the same principle still, though? Perhaps: std::sync::atomic::{AtomicUsize, AtomicIsize};
std::sync::atomic::b8::AtomicU8;
std::sync::atomic::b16::AtomicU16;
std::sync::atomic::b32::AtomicU32;
std::sync::atomic::b64::AtomicU64;
std::sync::atomic::b128::AtomicU128; These modules could all be conditionally defined based on the At that point it boils down to when we define We in theory may not support platforms that don't have any atomics at all quite that well, but it's sorta the same story for floats? Both the core/std variants are stable, so we unfortunately can't move either easily :( |
Actually I would prefer if we also gated the To keep things consistent the new I'll write up a new RFC for this soon. |
@alexcrichton I've created a new RFC: #1543 |
Thanks @Amanieu! I'll continue discussion of this proposal over there. |
With regard to monomorphisation, what's wrong with unsafe trait HardwareSupportedAtomic {}
#[cfg(target_has_atomic = "8")]
unsafe impl HardwareSupportedAtomic for u8 {}
#[cfg(target_has_atomic = "8")]
unsafe impl HardwareSupportedAtomic for i8 {}
struct Atomic<T: HardwareSupportedAtomic>; It's not as general-case as a sizeof-based solution but it at least allows crates that need it to opt-in on a case-by-case basis. |
🔔 This RFC is now entering its week-long final comment period 🔔 My own personal opinion is that I'd rather take the strategy proposed in #1543 over this one, but the |
Yep, without the type system growing an understanding of the size of a type, I'm not sure we can do better than what atomic-rs provides now. Should probably just keep in mind that this is a use case where a const size_of and type-level integers would be useful. |
I'm very late to this party, but I want to first of all thank @Amanieu for the excellent work on this RFC as well as the alternative. This RFC helped me better understand the motivation behind (Of course, That said, due to the various questions about how to handle fallback (or whether to generate errors at trans time), amongst other things, I feel like it's best to provide this kind of support outside of |
cc @rust-lang/lang |
Relevant issues: #1125 rust-lang/rust#24564
Relevant discussion in internals
Rendered