4

this good answer says:

volatile is completely unnecessary when used with std::atomic.

However, std::atomic_fecth_sub provides an overloaded function:

template<class T>
T atomic_fetch_sub(volatile std::atomic<T>* obj,
    typename std::atomic<T>::difference_type arg ) noexcept;

My question is:

If volatile is completely unnecessary for std::atomic, why does the C++ standard provide an overloaded function for it?

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
xmllmx
  • 39,765
  • 26
  • 162
  • 323
  • 7
    Well that's `std::atomic*`, not `std::atomic`. – Dean Seo Aug 22 '19 at 05:46
  • 1
    To me, the parameter `volatile std::atomic* obj` implies: In practice, the definitions such as `volatile std::atomic var;` are common. – xmllmx Aug 22 '19 at 05:48
  • 3
    Volatile does one thing. Atomic another. It's not inconceivable something might end up combining them. – Shawn Aug 22 '19 at 05:57
  • @Shawn I fully agree: Atomic guarantees that an op to the value is etiher not or fully done to the variable, volatile tells the compiler that the value may be changed by sby else (not my thread/ process) and so certain optimizations are not allowed. This is also explained in the referenced answer but kinf of made void by the introduction... – Mario The Spoon Aug 22 '19 at 06:01
  • 3
    @DeanSeo I first tapped into the same trap but then remembered how it is. Please, note that the `volatile` applies to `std::atomic` but not `std::atomic*`. Considering this, the question is IMHO justified. For a volatile pointer to something, it had to be `std::atomic *volatile` (and for a volatile pointer to something volatile `volatile std::atomic *volatile` or `std::atomic volatile*volatile` (because type specifiers and qualifiers may have any order)). I must admit these C++ type qualifiers are damn tricky... ;-) – Scheff's Cat Aug 22 '19 at 08:23
  • 1
    @Scheff Thanks for the correction! Just like `const` qualifiers, but my eyes are not used to `volatile` .... very interesting indeed. – Dean Seo Aug 22 '19 at 09:14
  • @DeanSeo Such `volatile` applied to a pointer parameter on a function declaration *that is not a definition* would accomplish nothing, just like a `const` on another scalar: `void f(const int); // meaningless` – curiousguy Aug 22 '19 at 16:55
  • @xmllmx "_volatile std::atomic var; are common_" They are seldom used (like volatile is seldom used). In practice compilers don't even optimize the most obviously redundant operations on atomics, so volatile semantics is already there. You can't count on it in future compilers obviously. – curiousguy Aug 22 '19 at 17:18

2 Answers2

1

If volatile is completely unnecessary for std::atomic,

Because the statement in question is relative to the question on which the answer was provided.

As far as atomicity and visibility of accesses within the C++ memory model is concerned (which is what the question was about), atomic is all you need. However, the use cases of volatile are principally about communication with external systems. Things like memory mapped devices and such which can be updated outside of the purview of the C++ memory model.

These features don't overlap. That is, volatile does not imply C++ atomicity/visibility, and C++ atomicity/visibility does not imply updating of the value from outside of the memory model. But it is hardly unreasonable to imagine a circumstance where you might need both C++ atomicity/visibility as well as external updating.

Maybe the atomic object lives in some shared process memory. The C++ memory model has no idea about things that happen outside of the program, so atomic alone isn't going to do anything about ensuring the visibility of operations from outside of the program. But volatile can.

volatile is not necessary for atomic to do its job within the C++ memory model. But for a particular user, they might need both atomic and volatile. Hence the overload.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • The `volatile` keyword implies exactly what the underlying CPU implies when dealing with a memory operation; a data race on a volatile object is OK if the CPU makes it OK (all CPU allow that). A volatile access is atomic off the CPU guarantees it's atomic, for a scalar. (For a compound type, there is no reliable mapping.) – curiousguy Sep 12 '19 at 13:01
0

Like for any scalar, volatile applied to an atomic object guarantees that assembly code exactly follow high level C or C++ code.

(Yes, it works the same in C and C++; no, there is no language called C/C++; yes, the design of many basic blocks is by intent the same in C and C++, and that includes the semantics of atomics.)

It means that no optimization is ever applied to these operations on atomic objects, and their effect is "immediately" "visible"; note that immediately is essentially meaningless in MT programs as operations can be reordered, so there is no well defined global "clock", and visibility depends on the guarantees of the CPU. But the order of operations on volatile qualified objects, including volatile atomics, as expressed in assembly/binary code, is never changed by the compiler, and you can check in the asm output or the disassembly - but you can't check at runtime in another thread as nothing is implied by volatile qualification on how changes to objects are made visible by other threads.

A way to formalize that "no optimization" guarantee of volatile semantics is to use a stopping signal on a program or thread, and then use a debugger, or ptrace like function, to examine particular objects; all volatile objects would have a state that is allowed by sequential execution of the paused thread, represented according to the selected ABI (when compiler flags allow for the selection of an ABI).

So volatile only makes sense for a particular ABI; the fact that volatile exists in C and C++ means that the part of the ABI that covers object representation is part of any standard discussion.

Calls to external, that is separately compiled functions (that means, by definition, compiled after all optimization, so that rules out global optimization), have the same level of guarantees of no optimization; they are done according to the calling convention of the ABI, and all objects that might be accessible by the called function are represented according to the object representation part of the ABI. (That's what I call an ABI boundary.) Compilers do not have to support separate compilation and a function calling ABI, but then if they don't, you can't even have both C and C++ as they are different languages based on absolutely distinct standards (with no shared references at the core language level) and the only way to mix them is at the ABI level. (There is no formal guarantee that extern "C" actually supports linking with any C compiler currently in use; the only formal guarantee is that these "C" calling convention functions can be called from the same C++ program.)

Any time you have to interact with anything other that code compiled by the same compiler you need an ABI convention; the volatile keyword was explicitly designed to allow interactions with a world even outside any compiler: the hardware external to the CPU world.

[Note: In C an C++ the volatile keyword can also be used for purposes of internal communication of the program between the executing program or thread and a signal handlers; or more rarely for programs that use longjmp which is obviously rare as it's essentially a (broken, leaking, unusable) exception system for C.]

curiousguy
  • 8,038
  • 2
  • 40
  • 58