26 ms·
> I think #define volatile is by far the sneakiest. The only legitimate reasons for using volatile generally involve memory-mapped IO and POSIX signal handlers
by psykotic 15y ago
> I think #define volatile is by far the sneakiest.
The only legitimate reasons for using volatile generally involve memory-mapped IO and POSIX signal handlers. C's volatile is nothing like Java's volatile and has very weak semantics. With lock-based code you generally have nothing to worry about because lock/unlock has load-acquire/store-release semantics. But if you're writing lock-free code, volatile is useless in general and you need to think carefully about how atomicity is affected by memory alignment and operand size (e.g. a byte write that might look atomic implicitly turns into a non-atomic read-modify-write), explicit memory barriers, etc.
So defining away volatile won't actually impact most applications unless they're relying on very compiler-specific behavior.
- tptacek 15y agoIt would definitely be a good way to fuck with kernel code, though.
- psykotic 15y agoThat it would. Lots of raw memory-mapped IO and dependencies on compiler-specific interpretations of C semantics!
- tptacek 15y agoI lost a couple hours of my life to a missing "volatile" writing code to talk to a network processor about 6 years ago, so this is kind of seared into my head.
- psykotic 15y agoYeah, I've done low-level driver and microcontroller code in C as well. But these days I view everything through a multi-threading lens, so my default reaction when I see people bring up volatile is to assume they're talking about its use in threaded code, which is unfortunately commonplace. Yesterday I fixed a latent data race in some lock-free code where a should_kill_thread-style variable had been marked volatile and the author had assumed that was necessary and sufficient to dispel any issues. Incidentally, that variable also had type bool and was in a struct next to another variable of type bool. Ruh roh...
- mattgreenrocks 15y agoWhat's the proper fix here? My knowledge of proper usage of volatile in C is weak. Does it have something to do with the fact that sizeof bool < sizeof int on most platforms?
- psykotic 15y agoThe trick is to be careful about variable size and alignment and use wrappers around platform-specific load/store operations with the right memory ordering guarantees. There's no portable way to do this in C. But whatever you do, volatile won't be involved. In that case I mentioned, yes, the issue is that sizeof(bool) = 1 by default on most compilers. You can force it to be 4 on MSVC with a compiler directive but that has its own problems (e.g. you are using a library which uses bools in its API and the library was compiled with sizeof(bool) = 1). The solution is to never use bool, int, etc, directly for variables that will be written to in lock-free code. Use typedefs (or even better for type safety, wrapper structs) that are explicit about size, make all lock-free reads and writes go through library functions/macros that provide the right atomic semantics. Here's the specific data race with the bools. Assume that sizeof(bool) = 1 and that the two aforementioned bool bytes (call them A and B) wind up next to each other in the same 4-byte aligned word of memory. Thread 1 writes to A and thread 2 writes to B concurrently. It looks like there should be no data race because they're writing to separate variables. But unlike storing a full word to memory, storing a byte involves an implicit read-modify-write, so the only way for this situation to be safe is if thread 1 and 2 use CAS operations to write A and B.
- mattgreenrocks 15y agoExtremely informative...thanks!
- tedunangst 15y agosizeof(_Bool) is probably 1. Whether someone is using stdbool.h or some other bool define (int is common enough) is unassumable. Not all CPUs can do atomic byte operations, so they're going to read/modify/write a machine word at a time. I'm not sure how much you'd have to mark volatile to get a compiler to behave the way you want, so you could use int and dodge the issue. Or better, the standard sigatomic_t type is provided and should work.
- wulczer 15y agoA typical usage of volatile, and one that bit me many times is using longjmp, for which the man page says: The values of automatic variables are unspecified after a call to longjmp() if they meet all the following criteria: · they are local to the function that made the corresponding setjmp(3) call; · their values are changed between the calls to setjmp(3) and longjmp(); and · they are not declared as volatile. I've had bugs that made me go half-crazy before someone reminded me that you have to use volatile in these situations.
- psykotic 15y agoThat's cool example I didn't know about. Thanks!
- mattgreenrocks 15y agoI assume that's a side effect of how the execution context is restored? If a nonvolatile local was stored in a register, and passed by address to a function that modified it before longjmp(), then the saved register value would clobber the updated one.
- psykotic 15y agoYou don't need to pass it by address to a function. Something like this could trigger it: // region 1: // x is allocated to register r1 int x = 0; // store 0 to r1 jmpbuf jb; if (setjmp(jb) == 0) { // region 2: // x is re-allocated to r2, so r1 is copied to r2 x = 42; // store 42 to r2 longjmp(jb, 1); } // region 3: // x is allocated to r1 printf("%d\n", x); // load from r1 The compiler is allowed to allocate the same variable to different locations at different points in the program. At re-allocation boundaries, it will insert register moves or stack spills or whatever. The problem in the example is that the longjmp() crosses a re-allocation barrier, so the store to r2 won't be registered as a store to x in region 3. Actually, this would still occur even if x was always allocated to r1. The reason is that before calling setjmp() the compiler spills x to the stack, so the callee can use the registers for its own use, and on returning (either via longjmp() or the first time through) it will restore x from the value on the stack. I tried the example above with GCC. At the default optimization level it printed 42. But with -O2 it printed 0. Looking at the assembly code, it's actually even worse than I described. The compiler has treated the longjmp() akin to an exit() and so has determined that the x = 42 is actually a no-op that can be eliminated. This is similar to how in printf("1"); exit(0); printf("2"); the compiler will actually remove the second printf() as dead code from the executable. So, in conclusion, there seems to be a whole bunch of ways in which the compiler could screw this up while staying within the bounds of standards-acceptable behavior.
- Symmetry 15y agoI think memory-mapped IO is a pretty frequent topic for readers of "Embedded in Academia". If you find yourself looking at real embedded code that doesn't have volatile somewhere that's a really bad sign. Locks won't prevent a memory access driving a SPI loop or UART from being optimized out, since C doesn't consider memory access to be inherently side-effectful.