Hacker News new | ask | show | jobs
by tialaramex 25 days ago
Volatile is a type system hack. They should have done a more principled fix, and certainly modern languages should not act as though "C did it" makes it a good idea.

The reason for the hack is that very early C compilers just always spill, so you can write MMIO driver code by setting a pointer to point at the MMIO hardware and it actually works because every time you change x the CPU instruction performs a memory write.

Once C compilers got some basic optimisations that obvious "clever" trick stops working because the compiler can see that we're just modifying x over, and over and over, and so it doesn't spill x from a register and the driver doesn't work properly. C's "volatile" keyword is a hack saying "OK compiler, forget that optimisation" which was presumably a few minutes work to implement, whereas the correct fix, providing MMIO intrinsics in the associated library, was a lot of work.

Why should you want intrinsics here? Intrinsics let you actually spell out what's possible and what isn't. On some targets we can actually do a 1-byte 2-byte and 4-byte write, those are distinct operations and the hardware knows, so e.g. maybe some device expects a 4-byte RGBA write and so if you emit four 1-byte writes that's very confusing and maybe it doesn't work, don't do that. On some targets bit-level writes are available, you can say OK, MMIO write to bit 4 of address 0x1234 and it will write a single bit. If you only have volatile there's no way to know what happens or what it means.

5 comments

I agree that marking the read/write as special rather than the variable itself would be nice, although it would also be nice if C/C++ was more consistent in the way things like this are done. Maybe given std::atomic and std::mutex as template/library features, supported by compiler intrinsics, it would be nice to have "volatile" supported in a similar way.

As a nit pick, I don't think this is correct use of "spill". Register spilling refers to when a compiler's code generator runs out of registers and needs to store variables in memory instead. In the MMIO case you are reading/writing via a pointer, so this is unrelated to registers and spilling behavior.

That's fair that "spill" probably isn't quite the right word.
By MMIO semantics do you mean explicit load and store instructions? I’ve never felt that pointer reads or writes were lacking descriptiveness here. I would argue the only surprising thing is that they might be optimized out (which is what volatile prevents).

Volatile on a non pointer value is not for MMIO, though, that’s typically for concurrency like with interrupts.

> I’ve never felt that pointer reads or writes were lacking descriptiveness here. I would argue the only surprising thing is that they might be optimized out

The C and C++ languages would be very slow by modern standards if you insist that reading or writing via a pointer must result in immediate fetches or stores to memory.

> Volatile on a non pointer value is not for MMIO, though, that’s typically for concurrency like with interrupts.

You're holding it wrong. Perhaps you've been holding it wrong for so long and so confidently that you've distorted the world around you -- indeed on MSVC on x86 or x86-64 that actually happened -- but, you're still holding it wrong.

> You're holding it wrong. Perhaps you've been holding it wrong for so long and so confidently that you've distorted the world around you -- indeed on MSVC on x86 or x86-64 that actually happened -- but, you're still holding it wrong.

Please explain. How would you make the variable backed by a hardware register region? Is this using some sort of linker trick to change where the value lives in memory?

You said it was for concurrency. The feature you want for that in C (and most languages suitable for this problem) is atomic memory ordering, not the volatile type qualifier.

Microsoft's platform was x86 only for years, and because Intel's design pays for a lot more memory ordering by default than most, on Microsoft's platforms just "volatile" would kinda work even though it was the wrong thing, so Microsoft explicitly grandfathered this for x86 and x86-64 only, you are guaranteed the Acquire-Release ordering even though you didn't ask for it with your volatile type qualifier.

If you were actually thinking of POSIX signals or something similar then yeah, the POSIX requirements say volatile will work, seems like a bad idea to me, but your compiler and other tools are likely also built for POSIX so they've read the same documentation.

We were talking about what these features were for when introduced. At the time there were no atomic instructions because there weren’t multiple concurrent execution paths or modern multi layer caching. At the time volatile on a scalar value would only be for preventing optimizations from the compiler assuming linear non-reentrant flow control. I would generally expect that to primarily be used for interrupt handlers in low level code, but posix signal handlers are similar.

Memory mapped registers are typically represented as pointers to volatile structs which I thing correctly represents that the device backing those addresses does not behave like main memory. Reading to or writing from those addresses needs to preserve the -O0 behavior of C where each pointer dereference must be preserved. I just don’t find anything particularly unclear about this, and I certainly don’t see any reason to make the caller have to be extra explicit about it when the reads and writes are already spelled out in the source code.

Yeah, it's also cleaner to be able to mark particular reads and writes as having side effects as opposed to having it be a property of the variable.
Thr Linux kernel uses READ_ONCE and WEITE_ONCE which look like actual function calls which is very sensible.
> The reason for the hack is that very early C compilers just always spill, so you can write MMIO driver code by setting a pointer to point at the MMIO hardware and it actually works because every time you change x the CPU instruction performs a memory write.

Source?

This is one of those "everyone doing this kind of work knows" that's rather hard to source, but: this is basically the point of volatile. Especially for reads rather than writes, where you may want to read some location that is being written into by a different piece of hardware.

People used to use it for thread synchronization before proper memory barrier primitives (see https://mariadb.org/wp-content/uploads/2017/11/2017-11-Memor... ) were available. It was not entirely reliable for this purpose.

Yeah. I could have sworn that I've read somewhere an anecdote from the Bell Labs era in which this comes up, but I can't find it and might be misremembering. The whole volatile keyword doesn't exist in K&R C as released, there are no "type qualifiers" at all in that language, both volatile and const are introduced in C89.

Duff's famous Device, often misunderstood as some insight about memory copying or something silly, was an MMIO hack, it doesn't look like an MMIO hack to us because it doesn't say volatile, but that's because Duff's compiler did not have that keyword, the reason Duff doesn't change the destination pointer is that it's pointing at hardware and the hardware isn't going anywhere, writing different bytes to the same address is I/O.

No idea about volatile, but I do remember function prototypes and const came as influence from C++, well CFront.
Source for what? The volatile keyword is explicitly telling the compiler "don't optimize read/write to this memory location". That's the whole point. Its use for manipulating hardware registers is covered in any intro embedded systems course. I don't know the history of C compilers but it would seem reasonable to assume that compilers started out plainly translating the C to machine code. Optimization would have happened later as the compilers became more mature.

https://www.gnu.org/software/c-intro-and-ref/manual/html_nod...

Source for "compilers basically always did volatile since everything was always spilled".