-
Notifications
You must be signed in to change notification settings - Fork 58
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
Stacked Borrows and VolatileCell #265
Comments
iirc The difference is that volatile reads are effectful, but reads to atomics/mutex/etc. are not. i.e. reading from volatile memory can have side effects. |
Indeed, though, I'd argue that "undefined behaviour" is effectful. While it's not necessarily a problem where a data race merely has compile-time effects, one could imagine an target where concurrent access to a memory location isn't permitted, and traps in some way (possibly with SIGBUS or an equivalent). |
I'd also note, I don't believe llvm allows |
https://releases.llvm.org/8.0.1/docs/LangRef.html
So spurious reads to |
Brilliant. I suppose that works fine in llvm, as a data race yields |
Indeed, as you observed, the Rust compiler is not allowed to insert spurious MIR-level reads in situations where that might introduce a data-race (basically, when there is an |
Indeed. My proposal is that, to support abstractions that depend on the read not occuring, we provide that such reads cannot be introduced in this case, unless they already exist within the function. It would also include that the compiler cannot permit llvm to introduce any reads either in this case (that is, when an I would add that, to benefit optimizations, merely the fact that the read operation occurs within the function (even if it's not evaluated), protected by any kind of reordering barrier, should be sufficient to allow non-volatile reads to be moved and introduced. I can't see a circumstance where optimizations could benefit from spuriously reading something that is never read by a function, or that is only read using Possible wording for this could be:
|
I would add that these rules would be necessary to have an operation be both atomic and volatile, IE. guaranteed for it's side effects, and well-ordered wrt. other threads of execution. (Something necessary for the api, as it has to communicate with interrupt handlers) |
I think we should stick to the principle that dead code has no effect. It will be really hard to have a proper Abstract Machine definition otherwise. I am also not sure why you are proposing a spec change; what is the intended effect of this? Do you want to make
|
Making VolatileCell, and it's wrapper AtomicCell sound is a primary goal. I
would note this was originally written prior to raw_ref!, do it could be
done with that, though the ergonomics are lost in that case. Having the
opt-out may be useful, though having this in some capacity provided by
UnsafeCell would be useful, as typically, you'd want interior mutability
when you need volatile access.
…On Mon, Dec 21, 2020 at 08:21 Ralf Jung ***@***.***> wrote:
merely the fact that the read operation occurs within the function (even
if it's not evaluated), protected by any kind of reordering barrier, should
be sufficient to allow [...]
I think we should stick to the principle that *dead code has no effect*.
It will be really hard to have a proper Abstract Machine definition
otherwise.
I am also not sure why you are proposing a spec change; what is the
intended effect of this? Do you want to make VolatileCell sound? There
has been lots of prior discussions on this and the two main ideas are:
- Have some kind of type which opts-out of dereferencable the way
UnsafeCell opts out of noalias.
- Ask people to use raw pointers instead of references, possibly a
more ergonomic form of raw pointers (&unsafe or whatever).
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#265 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABGLD24ZD2MGMG6SVF4ZE2LSV5DVDANCNFSM4U3UJ52Q>
.
|
Note that it is already possible to use volatile soundly by storing and passing the address (as a newtyped ptr or usize) rather than using the Cell pattern. Which isn't to say that the other proposed fixes can't still improve the situation further. |
As I noted previously, this solution fails if the address is not known at
compile time, merely attached to a symbol (and thus not known until link
time). This is one of the reasons I would like something along these lines.
The ability to place a definition over a magic symbol is far more important
to me than the ability to construct a magic address, as the latter only
works if it's known at compile time.
…On Mon, Dec 21, 2020 at 10:11 Lokathor ***@***.***> wrote:
Note that it is already possible to use volatile soundly by storing and
passing the address (as a newtyped ptr or usize) rather than using the Cell
pattern.
Which isn't to say that the other proposed fixes can't still improve the
situation further.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#265 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABGLD25F3IHXY47Q2OIJ2ETSV5QS3ANCNFSM4U3UJ52Q>
.
|
In that situation, something like this would probably work.
|
This pattern of The alternative, from what it sounds, is to make a wrapper type that stores only a pointer to the base of the memory region, then exposes the fields as a type that wraps the pointer to that field, which doesn't seem as nice as just modeling it as a regular struct type by using a Otherwise, it seems likely that a proc macro to generate an interface from a layout definition would be the easiest thing to do, which can be undesirable because of the impact that proc macros can have on compile times and its quite possible to have many such structs in a single crate that needs to access a lot of memory mapped devices. If anyone has any other suggestions for how this could be done though, do mention 👍 |
That's not really how you'd model it |
Do you have an example for how one would model it? |
Okay so I ended up having this chat with Repnop on Discord, so let me put down some notes. I'd like to prefix things with a big warning: Everything I'm about to suggest is about Rust as-is, not about how Rust should be. I'm describing the best setup to use in a world where &UnsafeCell is broken for MMIO, but I'm not saying that it should be broken for MMIO like it is. We can discuss the details of different possible fixes separately (because I also want MMIO to be much better than it is!), but for this post I'm focusing on "rust today". Okay so repnop (via Discord) had an example struct like this: #[repr(C)]
pub struct Plic {
source_priorities: [registers::Priority; 1024],
interrupt_pending: registers::InterruptPending,
_padding1: [u8; 3968],
interrupt_enable: [registers::Context<registers::InterruptEnable>; 15872],
_padding2: [u8; 57344],
threshold_and_claim: [registers::Context<registers::ThresholdAndClaim>; 15872],
_padding3: [u8; 8184],
} And then was thinking that you'd have something like: let my_plic_addr = VolAddress::new(0x123456 /* wherever */); And then you'd read and write That's absolutely too much data to be reading and writing all the time. A VolAddress should point to a type that's register sized or smaller. Something you can read or write as a single instruction. Not because it must, but simply because that's what's going to give you good performance, otherwise you're doing excessive reading or excessive writing. So what you'd do instead is similar to what repnop said, except that we'd manually "push down" the volatile property to the "fields" by making them methods which return the correctly offset address from the base address of our "struct". pub struct PlicAddr(usize);
impl PlicAddr {
// here, use min_const_generics, or possibly typenum crate on older compilers
pub fn source_priorities(self) -> VolBlock<registers::Priority, 1024> {
VolBlock::new(self.0)
}
pub fn interrupt_pending(self) -> VolAddress<registers::InterruptPending> {
// offset to this "field" manually
VolAddress::new(self.0 + size_of::<registers::Priority>() * 1024)
}
pub fn interrupt_enable(self) -> VolBlock<registers::Context<registers::InterruptEnable>, 15872> {
// offset manually
VolAddress::new(self.0 + size_of::<registers::Priority>() * 1024 + 3968)
}
// etc etc
} (In the case of an array, you'd use a "Volatile Block" type that allows further indexing from its own base address to a position within the memory block. This is provided in the I agree that it's more trouble to write it out like this, but it's a very obvious sort of code translation so a proc-macro (or even a macro-rules) can do it fairly simply. And this generally goes into a "hardware definitions" crate so it's not like you're re-running the whole proc-macro all the time. In terms of readability of this code style, your code would go from being something like p.source_priorities[5].read() into something more like p.source_priorities().index(5).read() Which is certainly close enough to my eyes. And again, I don't think Rust should always work like this, I just think that this whole dumb thing is what you have to do until there's whatever Language level changes made in the future to make MMIO not be a second class citizen. Personally, I'd just like a |
I really want to use this to do a non-atomic search for zero-values, followed by: compare_exchange_weak(0, new_value, Ordering::AcqRel, Ordering::Relaxed) If it fails, keep searching and try again. Thank you, @Lokathor. Voladdress is awesome! I'll use |
I have a type in a library for embedded development, called
VolatileCell<T>
, which sits over a memory address, and provides shared volatile access. However, I have been told this is unsound, as it uses&VolatileCell<T>
, which allows synthesized reads.This, however, got me thinking. Would this not also apply inherently to any
Sync
wrapper on an UnsafeCell, which offers mutation, such as Atomic or Mutex? After all, if the compiler can insert normal reads to the bytes behind an UnsafeCell, and a different thread performs even an atomic write that's not protected by a happens-before relationship, that a data race and immediate UB (according, at least, to the C++ memory model, which I have not heard rust diverge from enough to save this).I'm wondering if it makes sense to relax the rules for references, to "if a read occurs, it can be moved anywhere in the function (absent operations that bar reordering), even if the read may not be evaluated", rather than "the compiler can invent reads out of thin air", at least for things that contain an UnsafeCell.
Note: While the post is asking specifically about
VolatileCell
and a similarAtomicCell
which derives from it (and locks interrupts on reads/writes, giving interrupt-level atomicy), it generalizes to any such abstraction.The text was updated successfully, but these errors were encountered: