3

I saw a code sample demonstrating the usage of the volatile qualifier in the answer of the question C++ volatile member functions, quoted as below:

volatile int x;

int DoSomething() {
    x = 1;
    DoSomeOtherStuff();
    return x+1; // Don't just return 2 because we stored a 1 in x.  
                // Check to get its current value
}

I don't know if the volatile qualifier makes any difference in the above code. x is a global variable and there is a function call between the write and read on x, and we only read x for once. Shouldn't the compiler perform a real read on x (even it's not volatile)?

I think it's different from the following case:

volatile int x;

int DoSomething() {
    x = 1;
    while (x != 1)
        break;
}

In this case, we repeatedly read x immediately after a write to x, so volatile is necessary to get the up-to-date value of x written by other threads.

I'm not very confident on my understanding of these code samples, please correct me if I'm wrong.

EDIT (respond to comments): I'm sorry I didn't make my question clear. As for the first code snippet, I'm questioning whether the code is a valid example for the possible usage of volatile (not the guaranteed usage of volatile). I just want to know, without volatile, whether it's guaranteed that any possible change to x in DoSomeOtherStuff() can be reflected in return x+1, assuming there's no multi-threading or other non-trivial things like memory mapped IO. Cause if it is guaranteed to work without volatile, then the example is way off relevant, not to even mention the platform-dependent nature of volatile as some comments pointed out. But if it's not guaranteed, then I'm afraid some of my existing code may not work as I expected.
(I probably shouldn't have put the second code snippet at all.)

Community
  • 1
  • 1
goodbyeera
  • 3,169
  • 2
  • 18
  • 39
  • 1
    The C++ standard guarantees essentially nothing about the `volatile` keyword. (Try a search for "volatile is uselesss", which it is, if you are talking about portable multi-threaded programming.) So the behavior is entirely platform dependent. Please add a tag and/or update your question to indicate which compiler you mean. – Nemo Feb 19 '14 at 17:40
  • This is a good explanation on how volatile actually can be quite useful, exactly for multi-threaded programming: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766 – Mike C Feb 19 '14 at 18:14
  • @MikeC: That article is making assumptions about the semantics of `volatile` in multithreaded programs that are nowhere found in the C specification. If a particular compiler implementation happens to make `volatile` mean "introduces a memory barrier" or whatever is necessary to make a variable "thread safe" (whatever *that* means), that is an unportable and implementation-defined usage of the keyword. A better article to point people at is as Nemo suggests: http://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/. – Eric Lippert Feb 19 '14 at 18:38
  • @MikeC: The Herb Sutter article linked to by Dusty Campbell's answer is also good. Herb knows what he is talking about. – Eric Lippert Feb 19 '14 at 18:41
  • @Nemo It's not quite that bad. C++ makes it clear that `volatile` is part of the type system, and Andrei Alexandrescu found a way to exploit that for thread safety. The _intent_ is also very clear; it's just that there's no way of expressing it exactly in standardese. (Of course, the _semantics_ of `volatile` are irrelevant with regards to multi-threading.) – James Kanze Feb 19 '14 at 18:53
  • @EricLippert Except that the Intel article and Herb's contradict one another for some of the details. Where they disagree, the Intel article is correct. – James Kanze Feb 19 '14 at 18:58
  • @JamesKanze: In particular, Herb is assuming the semantics of `volatile` in Visual Studio, which can be used to implement (e.g.) double-checked locking. But the standard C++11 atomic operations, memory barriers, etc. are more portable and (when used correctly) more efficient, so there really is no use for `volatile` in modern C++. The only exception is totally platform-specific things like accessing hardware registers. (Although I suppose I should look at Alexandrescu's examples...) – Nemo Feb 19 '14 at 19:38
  • @JamesKanze: Good point, thanks for that clarification. – Eric Lippert Feb 19 '14 at 20:47
  • @Nemo and other folks: Thank you very much for redirecting me to lots of useful stuffs. I'm sorry I didn't make my question clear. As for the first code snippet, I'm questioning whether the code is a valid example for the **possible** usage of `volatile` (not the **guaranteed** usage of `volatile`). I just want to know, without `volatile`, whether it's guaranteed that any possible change to `x` in `DoSomeOtherStuff()` can be reflected in `return x+1`, assuming there's no multi-threading or other non-trivial things like memory mapped IO. – goodbyeera Feb 20 '14 at 01:31
  • (cont.) Cause if it is guaranteed to work without `volatile`, then the example is way off relevant, not to even mention the platform-dependent nature of `volatile` as you guys pointed out. But if it's not guaranteed, then I'm afraid some of my existing code may not work as I expected. – goodbyeera Feb 20 '14 at 01:36
  • @Nemo I don't think he is. For one, the article is from 2009; I know that Herb and others discussed many of the issues of `volatile` at the committee meeting in Berlin, before that, and that Herb was aware of the differences between what the standard requires, and what Microsoft was going to do. And the C++ `volatile` that he describes is _not_ the Microsoft `volatile` at the time (which was closer to the Java `volatile`). Globally, his points were well taken, but he still ascribed more constraints on the standard volatile than it has. – James Kanze Feb 20 '14 at 09:09
  • @Nemo The standard atomic types were introduced precisely because `volatile` didn't do the job. – James Kanze Feb 20 '14 at 09:10
  • 1
    @goodbyeera A compiler cannot assume that `DoSomeOtherStuff()` does not modify `x`, with or without `volatile`. The effect of `volatile` here is that even if the compiler can see into `DoSomeOtherStuff()`, and determine that the C++ code doesn't modify `x`, it cannot assume that `x` is unmodified. – James Kanze Feb 20 '14 at 09:12

4 Answers4

3

The volatile qualifier never makes a difference in the meaning of the code itself. Unless the compiler can prove that DoSomeOtherStuff() doesn't modify x, it must reread x regardless, volatile or no. For volatile to be relevant, x would have to be something like memory mapped IO, which might change outside the program. If we imagine that it is a register which is incremented every microsecond, for example:

int
MeasureExecutionTime()
{
    x = 0;
    DoSomeOtherStuff();
    return x;
}

would return the amount of time used in DoSomeOtherStuff; the compiler would be required to reload it, even if it inlined DoSomeOtherStuff, and saw that it never modified x.

Of course, on a typical desktop machine, there probably isn't any memory mapped IO, and if there is, it's in protected memory, where you cannot access it. And a lot of compilers don't really generate the code necessary to make it work correctly anyway. So for general purpose applications on such machines, there's really no sense in every using volatile.

EDIT:

Concerning your second code snippet: as usually implemented, volatile does not guarantee that you get an up-to-date copy of x. Arguably, this is not conform with the intent of volatile, but it is the way g++, Sun CC, and at least some versions of VC++ work. The compilers will issue a load instruction to read x each time in the loop, but the hardware may find the value already in the pipeline, and not propagate that read request out to the memory bus. In order to guarantee a new read, the compiler would have to insert a fence or a membar instruction.

Perhaps more importantly (since sooner or later, something will happen so that the value won't be in the pipeline), this looping mechanism will be used to wait until other values written in the other thread have stabilized. Except that volatile has no effect on when the reads and writes of other variables occur.

To understand volatile, it's important to understand the intent with which it was introduced. When it was introduced, memory pipelines and such were unknown, and the (C) standard ignored threading (or multiple processes with shared memory). The intent of volatile was to allow support of memory mapped IO, where writing to an address had external consequences, and successive reads wouldn't always read the same thing. There was never any intent that other variables in the code would be synchronized. When communicating between threads, it's normally the order of reads and writes, to all shared variables, which is important. If I do something like:

globalPointer = new Xxx;

and other threads can access globalPointer, it's important that all of the writes in the constructor of Xxx have become visible before the change in the value of globalPointer occurs. For this to occur, not only would globalPointer have to be volatile, but also all of the members of Xxx, and any variables member functions of Xxx might use, or any data accessible through a pointer in Xxx. This simply isn't reasonable; you would very soon end up with everything in the program volatile. And even then, it would also require that compilers implement volatile correctly, isserting fence or membar instructions around each access. (FWIW: a fence or a membar instruction can multiply time it takes for a memory access by a factor of 10 or more.)

The solution here is not volatile, but to make accesses to the pointer (and only accesses to the pointer) atomic, using the atomic_load and atomic_store primitives added to C++11. These primitives do cause the necessary fence or membar instructions to be used; they also tell the compiler not to move any memory access accross them. So that using atomic_load to set the pointer, above, will cause all preceding memory writes to be visible to other threads before the write to the pointer becomes visible (on the condition that the reading thread uses atomic_read, of course—atomic_write ensures that all previous writes are available in the "common" memory of all of the threads, and atomic_read ensures that all following reads will go to the "common" memory, and not pick up some value already in the pipeline).

James Kanze
  • 150,581
  • 18
  • 184
  • 329
0

A compiler can have some information about DoSomeOtherStuff() function. For example:

static int DoSomeOtherStuff()
{
    return 42;
}

volatile int x;

int DoSomething() {
  x = 1;
  DoSomeOtherStuff();
  return x+1; // Don't just return 2 because we stored a 1 in x.
  // Check to get its current value
}

GCC with -O3 option completely removes the call of DoSomeOtherStuff() function (and its body as well) but still reloads x to return x+1 in the end.

qehgt
  • 2,972
  • 1
  • 22
  • 36
0

Here is an existing SO question about volatile: What is the use of volatile keyword?

With just the first code snippet you are right, there is no obvious reason to have x declared volatile. However, if you understand what the keyword is supposed to be used for you could reason that x is volatile because there is something outside this code, which may change it's value. For example the memory maybe attached to some other hardware or written to by another program. Therefore, the programmer is instructing the compiler that it cannot see all the possible ways for the value of x to change. And so the compiler may not be able to optimize the code in certain ways.

The second code snippet in and of itself does not require the volatile keyword. Volatile is used as a compiler hint stating that the memory could change by forces outside the current program. It should not be used for thread communication. There are new C++ types that should be used for those situations.

I recommend reading this article by Herb Sutter as well as this talk.

Community
  • 1
  • 1
Dusty Campbell
  • 3,146
  • 31
  • 34
0

The only way to really be sure here is to examine the assembly code generated for your target platform. If volatile is performing the way you intend, any read of the variable x will be accomplished via a load from memory.

MattD
  • 380
  • 1
  • 15
  • From what memory? On a lot of machines, reads and such are pipelined, so that without a special fence or membar instruction, the hardware might not do an external read, even if the compiler generated a load instruction. – James Kanze Feb 19 '14 at 18:16