13

C11 6.7.3 Type qualifiers, paragraph 7, reads:

An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3.

In the following example, is the object accessed in the third line subject to the above rule?

int x;
volatile int *p = &x;
*p = 42;

In other words, does the fact that the lvalue *p has type volatile int mean that a volatile object is being accessed, or does the fact that p points to a non-volatile object x mean that the compiler can optimize with this knowledge and omit the volatile access?

Since it may be of interest, the particular usage case I'm interested in is outside the scope of plain C; it involves atomics for thread synchronization using pre-C11 constructs (which could be inline asm or simply thought of as a black box) for atomic compare-and-swap, with the following idiom:

do {
    tmp = *p;
    new = f(tmp);
} while (atomic_cas(p, tmp, new) != success);

Here the pointer p would have type volatile int *, but I'm concerned about what happens when the actually pointed-to object is non-volatile, particularly whether the compiler could transform the single access to *p from tmp = *p into two accesses of the following form:

do {
    new = f(*p);
} while (atomic_cas(p, *p, new) != success);

which would obviously render the code incorrect. Thus the goal is to determine whether all such pointed-to objects actually need to be volatile int.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • 2
    A conforming program won't be able to detect the difference, as accessing and modifying `x` are not observable side effects. Therefore, the compiler may optimize under as-if rule. – Igor Tandetnik Feb 22 '15 at 04:39
  • 1
    @IgorTandetnik: That's the way I lean towards interpreting it too, but it doesn't seem to be a popular theory, and I still question whether it's right. If `x` is never accessed except as `*(volatile int *)&x`, and `&x` is made visible to hardware that accesses it, or if `sig_atomic_t` is `int` and `*(volatile int *)&x` is accessed from a signal handler, then are there perhaps legitimate observable side effects? – R.. GitHub STOP HELPING ICE Feb 22 '15 at 05:56
  • 2
    If hardware accesses x and you declare it non-volatile then your hardware configuration is not implementing the C virtual machine. – philipxy Feb 22 '15 at 07:39
  • I don't see any side effects in the example you provided, I think the rule you mentioned, most taking about more complex objects than primitive int *. For example a pointer to a struct where you are manipulating the contents in a way other than explicit -> operator – madz Feb 22 '15 at 09:27
  • 1
    @philipxy: In many systems, certain objects will be subject to manipulation outside the compiler's control, only at specific identifiable times. Allowing a compiler to treat those objects using normal semantics except during those times when they might be changed by outside forces will allow code to be more efficient than would be possible if they needed to be processed using volatile semantics all the time. I find nonsensical the notion that in the name of "optimization" programmers should be required to prevent compilers from generating efficient code. – supercat Feb 18 '17 at 20:39
  • 1
    @supercat I don't think I understand the point you are trying to make; the question *defines* a non-volatile `int x` but you are talking about a volatile-defined object. For a *declaration* `extern int x` the standard says "If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined." The standard doesn't support your part-time volatile. – philipxy Feb 18 '17 at 23:45
  • 1
    @supercat PS I can imagine a compiler & linker pair that supports implementing a volatile-defined x source file & a non-volatile-defined x soruce file via a single part-time-volatile object. But such a compiler-plus-linker is not supporting the standard's semantics for that pair of source files defining "a" c program. – philipxy Feb 19 '17 at 00:18
  • 1
    @philipxy: As a common real-world situation, many programs need to assemble some data into a buffer, then trigger some outside process (e.g. a DMA controller) to transmit the contents of that buffer. Making the entire buffer `volatile` would prevent what should be useful optimizations in the code which builds the buffer contents, when all that is really needed is for all writes to the buffer to be complete before triggering a hardware cache flush(if necessary on the target platform) followed by the DMA operation) – supercat Feb 19 '17 at 00:38
  • 1
    @philipxy: If a compiler supports memory barriers, along with a means by which code says that it uses such barriers rather than relying upon `volatile` alone, then it would be better for code to use such barriers than rely on `volatile`. On the other hand, code which is designed for a compiler that's conservative around `volatile` will naturally work on other such compilers, without having to be explicitly adapted to any one in particular. – supercat Feb 19 '17 at 00:47
  • 1
    @supercat You are still stating a bunch of things but your point remains unclear. Volatile is for a very limited case of concurrency, the availablilty other mechanisms is irrelevant and your "will naturally work" is not supported by the standard. (But see my updated answer.) Your argument seems to be that it must be a certain way because that way is sensible or better but that is unsound. – philipxy Feb 19 '17 at 02:52
  • @philipxy: The authors of the Standard give implementions broad latitude to define the semantics of `volatile` in whatever fashion would be reasonable for their particular target platforms and application fields. They give just as much latitude to define semantics that make no sense whatsoever. I would suggest that quality implementations will behave in reasonable fashion, but since reasonableness depends upon factors outside the Standard's jurisdiction one must look outside the Standard to see what would be reasonable in any particular case. – supercat Feb 19 '17 at 22:44
  • @philipxy: If e.g. a particular platform had separate caches for its integer and floating-point units, then it may be reasonable for implementations *on that platform* to treat `volatile` writes of `float` and `double` as occurring in a separate universe from those of integer types outside of some situations involving character pointers. A quality implementation for a platform may also benefit from offering a non-conforming option which would disable synchronization on character-pointer accesses in favor of synchronizing on float-pointer or double-pointer to character-pointer conversions. – supercat Feb 19 '17 at 23:00
  • 1
    @philipxy: In any case, programmers who use `volatile` generally know what kind of hardware platform the code will be running upon, and on many hardware platforms treating `volatile` accesses as though they were preceded and followed by calls to a function whose internals the compiler knows nothing about would yield semantics that would be consistent with anything a programmer targeting such platforms might reasonably expect. – supercat Feb 19 '17 at 23:10
  • @R.. I just appended a note re single vs double "access". (I commented yesterday that I prefixed an update re standard vs rationale semantics.) PS Why do you say the tranform would give invalid code for `*p` non-volatile? If it's non-volatile, it can't change between the `do` read and the call in either version, so the transform is valid. (If it could, you must declare it volatile, regardless of the definition of "volatile access".) – philipxy Feb 20 '17 at 10:01

2 Answers2

9

Update 18 February 2017

The answer below quotes & discusses the language in the Standard, some contradictory language in the Rationale and some comments from gnu.cc re the contradiction. There is a defect report which essentially has committee agreement (although still open) that the Standard should say, and that the intent has always been, and that implementations have always reflected, that it is not the volatility of an object that matters (per the Standard) but of the volatility of (the lvalue of) an access (per the Rationale). (Credit to Olaf for mentioning this DR.)

Defect Report Summary for C11 Version 1.10 Date: April 2016 DR 476 volatile semantics for lvalues 04/2016 Open


No. Because the object accessed is not volatile.

Object p is of type pointer to volatile int. But x is not an object of a volatile-qualified type. The qualifications on p affect what accesses can be made through it, but do not affect the type of the object that it points to. There is no restriction on accessing a non-qualified-type object via a volatile lvalue. So accessing x through p is not an access of a object of a volatile-qualified type.

(See 6.7.3 Type qualifiers for the restrictions on accessing objects of qualified types. It just says you can't access a volatile qualified object via an unqualified lvalue.)

On the other hand, this post quotes from the 6.7.3 of the Rationale for International Standard--Programming Languages--C:

A cast of a value to a qualified type has no effect; the qualification (volatile, say) can have no effect on the access since it has occurred prior to the case. If it is necessary to access a non-volatile object using volatile semantics, the technique is to cast the address of the object to the appropriate pointer-to-qualified type, then dereference that pointer.

However, I can't find language in the standard that says that the semantics is based on the lvalue type. From gnu.org:

One area of confusion is the distinction between objects defined with volatile types, and volatile lvalues. From the C standard's point of view, an object defined with a volatile type has externally visible behavior. You can think of such objects as having little oscilloscope probes attached to them, so that the user can observe some properties of accesses to them, just as the user can observe data written to output files. However, the standard does not make it clear whether users can observe accesses by volatile lvalues to ordinary objects.

[..] it is not clear from the standard whether volatile lvalues provide more guarantees in general than nonvolatile lvalues, if the underlying objects are ordinary.

No, because there are no side effects:

Even if the semantics of *p must be that of a volatile, the standard nevertheless says:

5.1.2.3 Program execution 4 In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).

Again, there is no volatile object in your code. Although a compilation unit that only sees p couldn't make that optimization.

Also keep in mind

6.7.3 Type qualifiers 7 [...] What constitutes an access to an object that has volatile-qualified type is implementation-defined.

5.1.2.3 Program execution 8 More stringent correspondences between abstract and actual semantics may be defined by each implementation.

So mere appearances of volatile lvalues does not tell you what "accesses" there are. You have no right to talk about "the single access to *p from tmp = *p" except per documented implementation behaviour.

philipxy
  • 14,867
  • 6
  • 39
  • 83
  • 2
    The citation from the Rationale seems to indicate an intention that differs from the normative text... :-( – R.. GitHub STOP HELPING ICE Feb 22 '15 at 18:16
  • 2
    Even if an object is "ordinary", I would suggest that it is necessary to allow for the possibility that interrupts, other threads, etc. might need to access it with `volatile` semantics, subject to the constraint that non-volatile accesses may behave in unexpected fashion. There are real-world situations in which code needs to allocate buffers at run-time and have them accessed with volatile semantics. If a buffer returned from `malloc` were considered `volatile`, accesses via non-volatile pointers would invoke UB, so such a buffer must be "ordinary'. But if the ordinariness of... – supercat Jul 16 '15 at 17:38
  • 2
    ...the buffer meant that compilers could ignore `volatile` qualifiers on pointers to its contents, the semantics necessary to share data between contexts would be unachievable. – supercat Jul 16 '15 at 17:39
  • @supercat There is no such possibility. If it behaves in a shared manner then you need to declare it volatile. That's how you tell the compiler that it behaves that way. That's the language definition, otherwise the compiler can assume it's not shared/volatile. You are using UB (undefined behavior) backwards. When the standard says it's undefined, it's undefined and the program can do anything; you are simply describing a program for which the hardware is not behaving the way the language says it should behave. – philipxy Jul 16 '15 at 20:53
  • 1
    @philipxy: By what mechanism can code dynamically allocate memory for a process which needs to have `volatile` semantics, if not by using `malloc`? – supercat Jul 16 '15 at 21:17
  • @supercat "What constitutes an access to an object that has volatile-qualified type is implementation-defined." Implementation-defined behaviour and/or a library with volatile declarations (relying on it). (Maybe post a question version of your comments?) – philipxy Jul 16 '15 at 23:50
  • 1
    I wish I could upvote this a 2nd time for the update with that DR! – underscore_d Feb 20 '17 at 20:27
2

Not completely sure, but I think the point is the difference between the type an object has and the type an object is defined with.

From C11 (n1570) 6.3.2.1 p1 (footnote omitted, emph. mine):

An lvalue is an expression (with an object type other than void) that potentially designates an object; if an lvalue does not designate an object when it is evaluated, the behavior is undefined. When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. [...]

It's the lvalue which defines the type an object has for a particular access. In contrast, *p doesn't denote an object defined volatile. For example, ibid. 6.7.3 p6 (emph. mine) reads

[...] If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.133)

133) This applies to those objects that behave as if they were defined with qualified types, even if they are never actually defined as objects in the program (such as an object at a memory-mapped input/output address).

If the intent was to allow the code shown to be optimized out, the quote in the question would probably read An object that has [is defined with a] volatile-qualified type may be modified [...]

"Definition" of an identifier*) is defined in 6.7, paragraph 5.

Ibid. 6.7.3 p7 (What constitutes an access to an object that has volatile-qualified type is implementation-defined.) gives some leeway to implementers, but to me, the intent seems to be that the side-effect of modifying the object denoted by n should be considered observable by a conforming implementation.

*) Afaik the standard doesn't define "an object defined with (some type)" anywhere so I read it as "an object, designated by an identifier declared with (some type) by a definition".

Community
  • 1
  • 1
mafso
  • 5,433
  • 2
  • 19
  • 40
  • I'm leaning towards accepting this interpretation on the basis of your analysis of the word "has". Comparable language for `const` appears in the definition of "modifiable lvalue" where it "does not *have* a const- qualified type". In that case, if "have" were referring to the type the object was declared with in its definition rather than the type of the expression being used, `*(const int *)&x` would be a modifiable lvalue, which is obviously not the intent. There is some difference between "an object that has" and "an expression...that potentially designates an object [that] does not have" – R.. GitHub STOP HELPING ICE Mar 03 '15 at 20:27
  • but it still seems reasonable to me that the "object" in the text I originally cited is the object designated by the lvalue, having the type of the lvalue expression. – R.. GitHub STOP HELPING ICE Mar 03 '15 at 20:28
  • I would be wary of the "comparable language" analysis ; the standard was produced by many people via many edits and it seems unlikely that they would have made an effort to synchronize language for these two places – M.M Feb 19 '17 at 02:56