The reason people ask for this is because they haven't thought this trough.
Even if the compiler "preserves" all volatile operations, the HW does not.
A trivial example is WASM. Rust can generate WASM code that, when using volatile, has many duplicated loads and stores in some order. Then the browser runs a WASM optimizer on the WASM code, which discovers that these instructions are redundant, and there you go, your program now doesn't do what you wanted anymore (formally, it never did).
x86, ARM, PPC, FPGAs, GPUs, etc. all do this in HW as well. So while you could argue that, e.g., WASM should have volatile loads and stores, the same argument can be made at the next level down below, down to hardware.
You'd need to ask every hardware vendor for some sort of serializing loads and stores, and people have done that, and vendors have implemented that, but then when users finally got what they wanted, they complained that they are too slow, so at that point they just stop using volatile and use atomics which is what they should have done in the first place.
Since now I know that you knew that the feature you were requesting is impossible to implement, and you did not mention that in your feature request, now... i have no idea of what the goal of your comment was.
Importantly, the x86 ISA provides much stronger guarantees than C/C++. AFAIK the only memory reordering allowed is that a write followed by a read can be swapped around. This is extremely strict (we can’t even reorder adjacent reads) and literally any other processor architecture will provide much weaker guarantees. And because C and C++ formally defined a weak memory model in 2011, modern compilers are free (even when targeting x86 chips) to perform optimizations that reorder memory accesses in ways programmers familiar with x86’s semantics might not expect.
So an algorithm that worked on x86 processors 10-15 years ago may no longer be correct when compiled with a modern optimizing compiler, or when running on ARM. (If in doubt, you should be OK replacing anything volatile with an atomic and specifying sequentially-consistent ordering semantics for all operations — which you can do in Rust!)
[0]: https://systemtbe.blogspot.com/2014/05/volatile-memory-barri...
Java volatile is different of course.
Although, having to go all-volatile is maybe too much of a constraint, when many algorithms (like lock-free queues etc.) only require like 3 non-reordered accesses (2 to begin, 1 to end?) to safeguard a transaction, and then the remaining reads and writes can be reordered arbitrarily (inside the safe bounds).
No, you really won't, neither in C nor in Rust. There are (at least) two reasons for that:
1) You can't do atomic read-modify-write operations (like fetch_add) with volatile.
2) "volatile" is meant for the compiler, not for the processor. Volatile accesses are compiled down to regular read and write machine instructions, so you won't get any ordering guarantees for code running concurrently on different cores.
The reason "volatile" is useful for memory-mapped IO is that such memory is mapped with different flags for cacheability than regular memory. For example, given the right flags, the processor won't cache stores in a store-buffer, or won't coalesce smaller stores into larger stores before sending them up the memory hierarchy.
Are those operations so important? All I've ever done to be honest is implement some lock-free SRSW queues, and those can be easily implemented with just acquire/release semantics. No atomic read-modify-write needed. Could it be that those are only needed when more than 2 threads are involved?
Yes, absolutely. You can't even implement thread-safe reference counting (std::sync::Arc in Rust) without atomic RMW – or even a thread-safe counter.
A lock-free single-produce-single-consumer (SPSC) queue is pretty simple. As soon as you have more than one producer or more than one consumer, you need atomic RMW.
It's not "sufficient" for MMIO, it's literally the sole use case for it, and the sole situation where it's correct, because the hardware will not fuck with MMIO accesses either (and not eliding reads is important because reads might have side effects). Although even then you may need to use atomics anyway if the driver is concurrent, because volatiles don't guarantee any sort of atomicity and the reaction of MMIO devices to concurrent access is very likely to be funky (then again these days MMIO is usually a thing on uCs where there is no uncontrolled and arbitrary concurrency).
> Although, having to go all-volatile is maybe too much of a constraint, when many algorithms (like lock-free queues etc.) only require like 3 non-reordered accesses (2 to begin, 1 to end?) to safeguard a transaction, and then the remaining reads and writes can be reordered arbitrarily (inside the safe bounds).
Except your accesses will be reordered arbitrarily:
* volatile instructions are only of concern for the compiler, everything below it has no idea about them, and if you're not in an MMIO context they'll absolutely try to reorder and remove them (and even when they won't since volatiles are not synchronisation point you have no idea how they'll propagate)
* volatile instructions are not fences, even for the compiler are no "safe bounds" with volatiles unless all the operations are volatile
That's exactly what I said right? But good to know that volatile won't emit fences.
On x86 if everything that can be potentially shared is volatile, you may get away with it if you only need acquire/release, but in practice not everything being shared are not going to be volatile. For example if you implement a spsc queue only with volatile operations and use it to send a pointer to a non volatile object to another thread, the consumer might see a non initialized object.
That “(inside the safe bounds)” disclaimer is the reason we can’t use volatile for anything meaningful, because volatile does not provide that guarantee. The compiler & hardware will happily let the contents of your critical section float right past the volatile reads/writes “protecting” it.
If I write a lock-free queue and use volatile for synchronization, then I’m just protecting the queue itself and not the contents. For instance, if I write to some memory and then push a reference to it onto the queue, other cores might see the reference but not the data. Maybe the volatile writes got rushed through the memory hierarchy & to the other cores, but all the non-volatile writes are still stuck in a cache somewhere until next week.
And AFAIK volatile still doesn’t guarantee atomicity in the case of concurrent access. If one thread writes to a volatile variable, other threads can easily see torn writes/“pink elephants”/out-of-nowhere values. Again this is somewhere that x86 provides unnecessarily and somewhat ridiculously strong guarantees that nobody else does, so code that worked on dumb compilers in the 90’s may be very, very, broken today.
So please just use atomics. They’re very nice, and they solve all of these problems. If you don’t understand memory ordering semantics just mark everything as sequentially consistent and don’t think about it, I don’t care, just please use atomics.
The reads can always be reordered, unless they access uncacheable memory, which can happen only in device drivers, not in normal applications.
x86 provides 3 memory fences (LFENCE, SFENCE, MFENCE) which must be used when you need a certain ordering of the reads or of the reads around writes.
On x86, reads can be reordered between themselves only for non-temporal transfers.
For the normal case, when the accesses are made to cacheable read-write memory, the reads are not reordered between themselves.
Java's "volatile" is sequential consistency on steroids, a promise that changes to a volatile variable "happen-before" anything else that thread does, and "happen-after" anything the thread already did. Rust's std::sync::atomic has a hardware fence which you could use to deliver this same outcome (throw away performance, gain sequential consistency).
For concurrency C++ volatile does not supply any semantics, except on MSVC. If you're looking at papers or reference implementations which assume C++ volatile has semantics that are relevant to concurrency you're probably looking at papers that only needed Acquire-release and it worked on the x86 CPU they tried it with, or they forgot to mention it only works on MSVC (which promises Acquire-release even on platforms other than x86).
I mean, the box says: https://doc.rust-lang.org/stable/std/ptr/fn.read_volatile.ht...
> Rust does not currently have a rigorously and formally defined memory model, so the precise semantics of what “volatile” means here is subject to change over time. That being said, the semantics will almost always end up pretty similar to C11’s definition of volatile.
Which isn't exactly "exact", you know?
If so I feel that risk is small and continues to shrink.
But the semantics of "volatile" are completely different in C++ and Java! And I've seen lots of bugs from people assuming Java semantics for volatile in C++.
In C++ land, volatile is mostly useless and sometimes harmful (see [0]). It's dangerous to use it in concurrent code and hardly ever useful for memory mapped i/o without appropriate memory barriers (which make the volatile qualifier redundant).
In Java land, volatile can actually provide some useful guarantees when it comes to multithreaded uses but afaik they will usually just be turned into atomic load/store instructions so you might as well be writing atomics explicitly.
And it can be debated whether "volatile" should be a type qualifier in the first place. I would argue that it's better to make load/store operations on volatile memory explicit (for those very rare cases when you need it).
There is experimental support in Rust for the latter, `volatile_load` and `volatile_store` in `std::intrinsics` [1].
But still, doing volatile loads and stores in multithreaded code is hardly ever the right thing to do, at least without accompanying them with the relevant memory barriers (which are especially important on ARM CPUs).
[0] https://github.com/torvalds/linux/blob/master/Documentation/... [1] https://doc.rust-lang.org/std/intrinsics/fn.volatile_store.h...
It's not really experimental. You've linked the underlying core intrinsic, but the stabilised interface, which is what programmers should actually use, is std::ptr::write_volatile / std::ptr::read_volatile
Essentially you linked an implementation detail and that's why it's labelled unstable and you can't use it in stable Rust.
But yeah, you can have your volatile in Rust then. It's not a type level annotation, but an explicit (unsafe) operation.
That's not entirely true e.g. `read;read` can be very relevant in an MMIO context (a read can have side-effect), but atomic reads can technically be optimised out (though it might not currently be done by most compilers).
The memory model should explicitly forbid this optimization, especially if you use an explicit memory ordering with your loads and stores. I'm not 100% sure what the default (sequential consistency) does in this case.
And if you do `read(); barrier(); read(); barrier();` like you should on most CPU architectures, the compiler definitely is not allowed to reorder the reads (barriers affect the compiler AND the CPU). Which barrier you need depends on the MMIO semantics and CPU architecture you're on. Adding volatile does not change this but may inhibit other, useful optimization.
In your example of `volatile_read(); volatile_read()`, the compiler is not allowed to optimize or reorder the loads but the CPU is still allowed to reorder them with each other or other instructions. Adding `volatile` does not make this example work.
It has no reason to. Reading the same location twice, even atomically, has no “reason” to change the value so reusing the same read is perfectly valid.
If there is an other operation between the two and that has a sequencing implication then yes, otherwise no.
> In your example of `volatile_read(); volatile_read()`, the compiler is not allowed to optimize or reorder the loads but the CPU is still allowed to reorder them with each other
If there are memory reads then sure but i’m assuming actual mmio here (since volatile is otherwise useless) and i’d think an mmio implementation which reorders mmio reads is completely broken.
No, this is definitely not how atomics with memory orderings work. If you do two atomic reads, the CPU will do two load instructions. Neither the compiler or the CPU is allowed to optimize this.
> and i’d think an mmio implementation which reorders mmio reads is completely broken.
This is exactly how ARM SoCs work. The CPU core does not know if a load address is mmio or not and may reorder the loads as it sees fit. It's up to the memory subsystem to deal with this, the CPU core does not care.
I've done lots of mmio code on ARM at $work in the past few years, and we've spent a lot of time to make sure we have all our memory barriers right. They are quite tricky on ARMv8, as mmio can be either on the system bus or the main memory bus which need distinct kinds of memory barriers.
Using volatile in mmio code on modern CPUs is almost certainly a bug. Some microcontrollers may be an exception.
Of course it is.
> If you do two atomic reads, the CPU will do two load instructions. Neither the compiler or the CPU is allowed to optimize this.
If you perform two atomic loads on the same location in sequence, then it is completely feasible and normal that the two loads would return exactly the same value, thus the second load can be optimised away. Even under sequential consistency, this is perfectly, well, consistent. It's also perfectly valid to fold atomic writes. See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n445... for more discussion on the subject
No compiler currently bothers doing that (that I know), but it's not because they can't, it's because the effort is probably not worth it, at least at the moment.
> I've done lots of mmio code on ARM at $work in the past few years, and we've spent a lot of time to make sure we have all our memory barriers right. They are quite tricky on ARMv8, as mmio can be either on the system bus or the main memory bus which need distinct kinds of memory barriers.
I can believe that.
> Using volatile in mmio code on modern CPUs is almost certainly a bug. Some microcontrollers may be an exception.
Not using volatile in mmio code on modern compilers is almost certainly a bug as well.
Yes they are. And other optimizations are allowed too. If you do an 32-bit atomic load and then mask the result with `& 0xff`, the compiler can theoretically narrow it into an 8-bit atomic load, as long as the architecture guarantees that such a load still provides the necessary ordering guarantees – which I think is true on most architectures. This won't necessarily work for MMIO, where incorrectly-sized accesses may trap or return the wrong data. But the compiler is not responsible for preserving MMIO semantics if you only asked for atomic.
That said, I'm not aware of any major compilers performing /any/ significant optimizations on atomics, so you won't get in trouble for this in practice.
> This is exactly how ARM SoCs work. The CPU core does not know if a load address is mmio or not and may reorder the loads as it sees fit. It's up to the memory subsystem to deal with this, the CPU core does not care.
You can configure in memory attributes whether to allow reordering or not.
> They are quite tricky on ARMv8, as mmio can be either on the system bus or the main memory bus which need distinct kinds of memory barriers.
Which SoC?
Meaning they’re completely different and way incompatible with Java’s.