-
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
Generic atomic #1474
Generic atomic #1474
Conversation
Presumable we'd want libstd to define those language items? If so, how would they be implemented? |
They behave like the equivalent atomic operations.
There are many ways to implement this. |
But you can't have multiple crates provide the same language item; so one of those ways has to be picked in libstd. Or do you want this feature to be only usable in applications, not in libraries? |
The RFC does not dictate how the functions are implemented. Only how they behave. |
One intrinsic is added: | ||
|
||
```rust | ||
fn has_native_atomic_ops<T>() -> bool; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be reasonable to make this intrinsic const
?
The design seems a bit ... incomplete? When I read the part about introducing new (unsafe) langitems to do the heavy lifting, I expected a nice and safe interface type with proper trait bounds (e.g. As C++'s version of this is template-based, I wonder if there's a nice way to do this using impl specialization (RFC #1210) instead of langitems. |
The language items are an implementation detail of the corresponding intrinsics. User code never calls the language items directly. It calls the intrinsics. The intrinsics don't have any such bounds since they are low-level. Just like
Not unless there is a trait that encodes |
You can see an implementation of |
@mahkoh Okay, so all this RFC does is handle the case when code using any |
Yes.
How and if a type such as |
Updated |
Echo concerns of others, I feel like the signature given isn't necessarily enough: it seems to me that making the given operations atomic for an arbitrary Library concerns/"high level details" definitely aren't orthogonal/irrelevant to this RFC: language features need to be designed to be appropriate for the various library features that want to be built on top of them. |
The implementations of the functions are trivial and already described in the RFC. static LOCK: Lock = ...;
fn atomic_load<T>(src: *const T) -> T {
let _guard = LOCK.lock();
ptr::read(src)
}
Only implementation details of the various intrinsics that already exist are introduced. No library will never interact with them. |
Yes, that implementation is mentioned as an example of how to implement it, but there's more to being OK than some implementation existing. I mentioned an alternative in my comment which seems like it would be significantly better along several major axes, such as not globally serializing all of these atomic operations. If we were going to have a signature that basically forced the operation to have that implementation, it might make more sense to make the mutex itself a language item (or init/lock/unlock functions), which would give more flexibility, e.g. there could be a mutex for each type
So... you're saying no library will ever include an implementation of these language items? This is clearly untrue, and so the feature needs to be flexible enough for libraries that do implement the language item (which will be a small set) to do it in the best way for them. |
More flexible implementations are also possible e.g. by computing a hash of the address.
Congratulations, you managed to twist my words.
What's that? The most space efficient way? The fastest way? The one that minimizes the cost on both axes? For every given implementation you can find pathological cases, e.g., type-based locks will perform very poorly in an application that uses only one type compared to using address-based locks. An efficient higher level implementation requires impl specialization so that |
This isn't very convincingly "OK" to me: it seems like it would quickly leak a pile of memory if atomic values end up being relatively dynamic in memory. This is certainly true for e.g. concurrent queues, but these use values for which hardware has the appropriate instructions, so the precedent from such examples may not be relevant. I have no experience about what is done elsewhere (e.g. C++), so this could well be reasonable in practice.
I'm not really sure what your original point is supposed to be. This is a language feature which picks up functions from libraries, and so it seems pretty reasonable to consider how libraries might want to implement those functions.
Yes, that's basically my point: the current signature doesn't give much way to choose which trade-offs you want. (With current Rust, AFAICT, the signature can basically allows you to choose between a global lock and a concurrent map from addresses/typeids/something else to a lock.)
I'm not really sure it is: once such an |
How does it leak memory? You simply use a static array of N booleans (N = 16 or 32 or whatever), each of which is used as a spinlock. Which boolean you choose depends on the address of the atomic.
Higher level libraries (such as the ones implementing
You have not yet shown a more flexible signature.
Then remove it. This RFC merely introduces implementation details of the atomic intrinsics. |
That does seem more reasonable than the impression I got from your initial comment, thanks for being more explicit.
I was and am talking about the low-level ones that have to choose how implement these details, since as you've said repeatedly, other libraries generally don't care about this detail and are hence not particularly relevant.
One could add #[lang = "unsupported_atomic_load"]
fn atomic_load<T>(x: *const UnsupportedAtomic<T>) { ... }
(However, this is clearly complicated, especially for a temporary measure.)
That's not answering my question, but I infer from the dismissal that I'm not missing any other important trick this is useful for, and this RFC is purely a stop-gap until we can do However, in that case, why can't the temporary hole be filled by a trait like |
This sounds like But if you're willing to leak those implementation details then I've already hinted above at a simple solution that uses impl specialization: trait AtomicOps<T> {
fn load(&self) -> T;
....
}
impl<T> AtomicOps<T> for Atomic<T> { ... }
impl<T: HasNativeAtomicOps> AtomicOps<T> for Atomic<T> { ... } where
It cannot because it requires knowledge of how the type is represented in memory to decide whether it can use native atomic operations. Such knowledge is, in general, unavailable outside the compiler. |
Yes, but this is, I believe, relatively simple, at least compared to impl specialisation.
I don't understand your point: if that worked, this whole discussion would be totally moot. We currently do not have impl specialisation, and so this doesn't work (and I don't think just making a single trait a lang item to allow specialisation will work: there's a pile of impl selection infrastructure in the compiler that needs to understand it to find the right code to run). In any case, that's exactly what we've both been talking about with impl specialisation, and exactly what I was referring to when I mentioned that the manual trait design could be expanded to automatically use impl specialisation when it works.
Yes, I was quite explicit that this requires manual intervention and hence won't be perfect, but it allows you to progress without (waiting for) hacks in the compiler. You can deduce which types have atomic operations for most types in a simple manner, and, anyway, I suspect the vast majority of atomic values will be the obvious ones: primitives and pointers. This serves as a 90% (or 95%, or more) solution, until the 100% solution of impl specialisation works. |
The 90% case already works. This RFC is for the 10% case where the compiler crashes because the type doesn't have native operations. |
Allowing If #![crate_type="rlib"]
#![feature(core_intrinsics)]
use std::intrinsics;
use std::ptr;
pub unsafe fn load_u32(u: *mut u32) -> u32 {
atomic_load(u)
}
pub unsafe fn load_u64(u: *mut u64) -> u64 {
atomic_load(u)
}
pub unsafe fn atomic_load<T>(t: *mut T) -> T {
if intrinsics::type_name::<u32>() as *const _
== intrinsics::type_name::<T>() as *const _{
intrinsics::atomic_load(t)
} else {
global_lock();
let result = ptr::read(t);
global_unlock();
result
}
}
extern {
fn global_lock();
fn global_unlock();
} |
pub unsafe fn atomic_load<T>(t: *mut T) -> T {
if intrinsics::type_name::<u32>() as *const _
== intrinsics::type_name::<T>() as *const _{
ptr::read(t) That's just undefined behavior. if intrinsics::type_name::<u32>() as *const _
== intrinsics::type_name::<T>() as *const _{ That doesn't help since the very point is that it should work for most types <= ptr size.
Another case where rustc is worse than gcc/clang. |
That was just me being lazy to illustrate the point. Fixed to use
I agree that fully general untyped templates give C++ a big advantage in some places. We think the disadvantages of it are greater. |
Nobody is talking about C++. Unless you think that C also has untyped templates, your comment doesn't make much sense. |
Superseded by #1477 |
Add compiler support for generic atomic operations.