13

In this example, does correctness require global_value to be declared volatile?

int global_value = 0;

void foo () {
    ++ global_value;
}

void bar () {
    some_function (++global_value);
    foo ();
    some_function (++global_value);
}

My understanding is that volatile is "intended" for pointers to mapped memory and variables which can be modified by signals (and emphatically not for thread-safety) but it's easy to imagine that bar might compile to something like this:

push EAX
mov EAX, global_value
inc EAX
push EAX
call some_function
call foo
inc EAX
push EAX
call some_function
mov global_value, EAX
pop EAX

This is clearly not correct, but even without volatile I believe it is valid according to the C abstract machine. Am I wrong or is it valid?

If so, it seems to me that volatile is routinely overlooked. This would be nothing new!


Extended Example

void baz (int* i) {
    some_function (++*i);
    foo ();
    some_function (++*i);
}

int main () {
    baz (&global_value);
}

Even if bar is guaranteed to compile into a correct dont-cache-global_value implementation, will baz be similarly correct, or is it allowed to cache the non-volatile value of *i?

Community
  • 1
  • 1
spraff
  • 32,570
  • 22
  • 121
  • 229
  • The exact semantics of `volatile` are implementation-dependant – Seb Holzapfel Jul 28 '11 at 11:36
  • `baz` has nothing to do with volatile, it is a standard aliasing problem that the compiler knows all about. C99 has `restrict` because of this. The best way to avoid these problems is not having global variables at all! – Bo Persson Jul 28 '11 at 16:32
  • 2
    It's not restricted to globals, class members can have nonlocal modifications in the same mannar via other members or friends. I suppose the solution is "paranoia or whole-program-optimisation" then... – spraff Jul 28 '11 at 16:36

5 Answers5

12

No, the volatile keyword is not necessary here. Since global_value is visible outside the function bar, the compiler must not assume that it remains the same if another function is called.

[Update 2011-07-28] I found a nice citation that proves it all. It's in ISO C99, 5.1.2.3p2, which I am too lazy to copy here in its entirety. It says:

At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.

Sequence points include:

  • The call to a function, after the arguments have been evaluated (6.5.2.2).
  • The end of a full expression: [...] the expression in an expression statement (6.8.3); [...]

There you have your proof.

Roland Illig
  • 40,703
  • 10
  • 88
  • 121
  • 1
    Do you have a citation for that? – spraff Jul 28 '11 at 11:42
  • 1
    No, I don't. This whole code seems so obvious to me (assuming you have a single-threaded program) that I don't even know in which section of the C standard I had to look it up. – Roland Illig Jul 28 '11 at 11:45
  • 2
    You could perhaps cite the fact that there's a sequence point on return from `foo`. Hence the code in `foo` must write the object before then. The code in `main` after the call to `foo` must use the actual value of `global_value`, established at that sequence point, not some different value. It's hard to say where in the standard it says, "when you say `global_value`, that means `global_value`, not some prior value of it, or for that matter some other number pulled out of thin air". As Roland says, that's obvious in single-threaded code, which is all the standard addresses. – Steve Jessop Jul 28 '11 at 11:53
  • 1
    All the occurrences of the identifier `global_value` refer to the same object, and all accesses are properly separated by *sequence points*. This could be a hint for a complete prove. That would definitely be an interesting task for a language lawyer. – Roland Illig Jul 28 '11 at 11:55
  • So is it true that all global values are re-fetched at sequence points? *That* might be easier to cite, and is a sufficient condition. Please see my Extended Example -- does the same argument apply? – spraff Jul 28 '11 at 12:14
  • Updated my answer. Yes, all previous effects on global values (and all other objects, by the way) have taken place whenever a sequence point is reached. – Roland Illig Jul 28 '11 at 12:48
  • 1
    @spraff: it is not true that all global values are re-fetched at all sequence points. The compiler is allowed to make deductions about what side-effects are possible, and may cache if it is not possible that the value is changed. – Steve Jessop Jul 28 '11 at 14:48
  • Thank you but I don't think it's proof. All the sequence point guarantees is that the value of `global_value` in `foo` will account for the completed first increment in `bar`. AFAIK it does not guarantee that `global_value` is invalidated *by* `foo`. – spraff Jul 28 '11 at 14:49
  • It does. `++global_value;` is an expression statement, and immediately after this statement, there is a sequence point, at which the incremented value is guaranteed to be stored. After that sequence point the evaluation of the arguments for the call to `some_function` take place. – Roland Illig Jul 28 '11 at 15:09
  • 1
    And to understand the proof fully, these few citations are not enough. You also need to understand them in the context of the other >1000 paragraphs in the C standard. – Roland Illig Jul 28 '11 at 15:12
  • Asking for a "proof" of that is kind of ridiculous. It's obvious. Or else, please explain why you think it would not hold. – curiousguy Oct 04 '11 at 00:06
5

The only uses of volatile involve longjmp, signal handlers, memory-mapped device drivers, and writing your own low-level multi-threaded synchronization primitives. For this last use however, volatile is not sufficient and may not even be necessary. You'll definitely also need asm (or compiler-specific or C1x atomics) for synchronization.

volatile is not useful for any other purposes, including the code you asked about.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • Volatile is "certified" for these uses, and was invented for MMIO; but it's useful for plenty of other purposes. – curiousguy Dec 13 '19 at 00:43
  • @curiousguy: Care to give an example? – R.. GitHub STOP HELPING ICE Dec 13 '19 at 01:17
  • F.ex. volatile can be used for threads pretty much like for MMIO, with almost the same issues. – curiousguy Dec 13 '19 at 01:28
  • @curiousguy: I stated in the answer that there are some ways you can use it in synchronization between threads, but it's neither necessary nor sufficient for that, only an ingredient you can potentially use if you're rolling pre-C11 synchronization. I was hoping for an example outside the things covered in my answer. – R.. GitHub STOP HELPING ICE Dec 13 '19 at 01:51
  • You use volatile whenever you want to avoid all optimizations on accesses to an object; it can be used for weak inter thread signaling, to prevent values to be known during compilation, to get the CPU addition (2 compl) on an integer addition, to ensure a variable isn't optimized out, to make an object modifiable in the debugger, to be able to see reliably see objects with ptrace, to be sure some object follows the ABI representation... the uses are unlimited. – curiousguy Dec 13 '19 at 03:55
4

As Roland says, I'm not sure what part of the standard to cite to say, "if a program modifies something, that means the object is modified in the abstract machine. If a program uses a value, that means it uses whatever value the object has in the abstract machine".

volatile controls the number and order of reads and writes to memory, but even without volatile, an implementation that caches values as an optimization must respect the behavior of the abstract machine. That's what the "as-if" rule says, so optimizations that don't obey that aren't "easy to imagine" for me ;-) Your proposed emitted code is as clearly wrong to me as saying, "a write might go to memory without updating or dirtying the L1 cache, so future reads will still see the old value in the cache". Not on a single core, it won't, because a cache that behaved like that would be broken.

If you call strcpy, and then examine the contents of the destination buffer, the compiler isn't allowed to "optimize" by using a prior value of that byte, stored in a register. strcpy doesn't take a volatile char *. Similarly, global_value does not need to be volatile.

I suppose the confusion may be that in multi-threaded code, "and then", which is to say whether the read occurs "after" the write and hence "sees" the new value, is defined by synchronization primitives. In some implementations, volatile has something to do with synchronization due to implementation-specific guarantees.

In single-threaded code, and in the C and C++ standards, "and then" is defined by sequence points, of which there are plenty in the code given.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • I don't think `strcpy` is a reasonable comparison because passing a non-const pointer into a function implies an intent to change that object. That argument doesn't apply to the Extended Example I posted just now either because the pointer is not being passed into `foo` from `baz`. – spraff Jul 28 '11 at 12:17
  • 3
    @spraff: I'm trying multiple ways of telling you the same thing. If one of them doesn't click, then whatever :-) No, in your extended example the value of `*i` can't be cached. The compiler can only cache a value across a call if it knows that (in the abstract machine) the call does not modify the value. That's elementary correctness, it's nothing to do with volatile. – Steve Jessop Jul 28 '11 at 14:44
  • @spraff "_an intent to_" Hug? Well defined behaviour is not about "intent". The compiler must follow relevant standards, **not guess programmer's intent**. You seem to confuse Computer Science with Psychology. Strange. – curiousguy Oct 04 '11 at 00:12
1

No. Global variables should not always be declared volatile.

You only really need it to be volatile if it could be changed by other threads and may suffer from memory reordering issues or compiler instruction reordering. And even then you won't need it if you have appropriate mutexing. Typically though, you probably have a bad design if you need to mutex global variables.

EDIT: making it volatile does not mean that the global variable would be thread safe though!

Other typical uses might be where the memory is accessed in an unusual way - for example if you have some DMA mapped memory on an embedded micro.

Pete
  • 4,784
  • 26
  • 33
  • 1
    Regarding your 2nd paragraph: you are already doomed if you do `++global_value` from multiple threads and you *don't* have appropriate mutexing, regardless of whether it's `volatile` or not... – Matthew Slattery Jul 28 '11 at 20:49
  • If that's your exact point, I'd suggest editing your answer for clarification, because it's not what you've written: by saying "You only need it to be volatile if it could be changed by other threads [etc.] ... *even then* you won't need it if you have appropriate mutexing ..." you are implying that if you *don't* have appropriate mutexing, then you *do* need it to be volatile. But, without a mutex, you don't "need it to be volatile": multiple threads cannot safely do `++global_value` at the same time, regardless of whether `global_value` is `volatile` or not. – Matthew Slattery Jul 30 '11 at 00:19
  • @Matthew - fair point - it isn't clear but you are clearly correct - making it volatile does not mean it is thread safe. – Pete Aug 02 '11 at 11:40
0

Volatile isn't needed in this example. If, for instance, some_function() outputs something, the asm listing seems changes observable behaviour of the c++ machine and violates the standard.

I guess it is a compiler bug Here is GCC assembler output:

.cfi_def_cfa_register 5
subl    $24, %esp
.loc 1 67 0
movl    global_value, %eax
addl    $1, %eax
movl    %eax, global_value
movl    global_value, %eax
movl    %eax, (%esp)
call    _Z13some_functioni
.loc 1 68 0
call    _Z3foov
.loc 1 69 0
movl    global_value, %eax
addl    $1, %eax
movl    %eax, global_value
movl    global_value, %eax
movl    %eax, (%esp)
call    _Z13some_functioni
.loc 1 70 0
leave
.cfi_restore 5

global_value is reloaded, as expected, between function calls

Also volatiles are for thread-safety too, simply v-qualifier is not sufficient for thread safety in all cases (you sometimes need additional care about atomicity and memory barriers, but interthread communication variables should be volatile...

[EDITED]: ... if they are repeatedly read and may be changed by another thread between reads. This, however, is not the case if any syncronization lock (mutex, etc) is used, since lock guarantees the variables can not be changed by any concurrent activity) (thanks to R..)

user396672
  • 3,106
  • 1
  • 21
  • 31
  • 1
    Last paragraph is completely wrong. All locking primitives are complete compiler barriers. No object which could be accessible from another thread can be cached across lock/unlock calls. There is no use in `volatile` with normal, standards- (e.g. POSIX) conformant thread use. The only time `volatile` is useful with threads is when implementing your own low-level synchronization primitives, which will also require asm or compiler-specific synchronized memory features. – R.. GitHub STOP HELPING ICE Jul 28 '11 at 12:31
  • @R.. I mainly had in mind the last case, i.e. code without external synchronization locks.. thank you, edited. – user396672 Jul 28 '11 at 12:43