5

This is a question about ISO C, which contains this sentence:

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.

In ISO9899:1999 (C99) this appears in 6.7.3 Type Qualifiers.

Firstly, if we entirely removed this sentence from the text, would the behavior still be undefined? That is to say, is this re-stating an existing lack of a requirement, or is it explicitly taking away a requirement that could otherwise be inferred from elsewhere in the text? (Imagine that a sentence were added which says "whenever two values of type int are added together whose arithmetic sum is forty-two, the behavior is undefined". Without such a sentence, the requirement for such an addition to produce a value can (and is) inferred).

Secondly, what useful behaviors does this allow an implementation to provide? That is to say, can an implementation do something beneficial which breaks programs that access something volatile with a non-volatile lvalue, and which therefore depends on the above sentence? Are there any implementations which do this, whatever it is?

For example, it is interesting and noteworthy that C programs can access const-qualified objects with non-const lvalues, just not store to them. Making the store behavior undefined is tangibly beneficial: implementations can place constants in read-only storage, and in some cases can propagate constants through a program image at translation time as if they were manifest constants. Yet, mere non-const access is required to work. Not requiring non-const accesses to const objects to work would presumably be a permissiveness which brings no benefits. Yet, in the case of volatile, it is that strict: no non-volatile-qualified access is required to work. What is the benefit?

Can an object which is defined as volatile by the C program itself be endowed with useful properties which break if there take place any accesses whatsoever to that object through a non-volatile lvalue?

(Under "useful", I would specifically like to exclude diagnosis. Terminating the program upon detecting such an access, without a message, or with a message like "volatile accessed as non-volatile" doesn't count as useful; it serves only to enforce the lack of requirement.)

(Also, I'm not interested in memory mapped registers and such; strictly objects defined by the program in static, dynamic or automatic storage. Use of hardware registers is inherently nonportable.)

All I can think of is that a non-volatile access to a volatile object could retrieve a stale value, rather than value that was last stored (properly, through the volatile lvalue). This is an economic benefit in the following way: if a write occurs through, say, a volatile int lvalue, the compiler does not have to suspect that this has any effect on any plain int lvalue. This leads to better code generation in functions which work with mixtures volatile and non-volatile objects of the same type:

volatile int global;

void foo(int *p)
{
   int x = *p, y;
   global++; 
   y = *p;
}

Here, the compiler doesn't have to consider that *p might be an alias for global, because that usage is not required to work according to ISO C. Hence it can freely assume that the value of *p does not change between the two references. Both x and y receive to the same value, derived from a single access to *p.

If the aliasing were permitted, then the compiler would have to generate code to re-load *p on the second access, in order that it pick up the most recent value stored in global.

(If we remove volatile from the above program fragment, then this is in fact the case; it has to be at least suspected, if not confirmed. that *p may be an alias for global and so *p has to be re-inspected. Or else code has to be generated which compares the run-time value of p to the address of global and takes two different paths.)

However, we don't introduce volatile into programs to obtain this kind of obscure optimization benefit; it is used to defeat optimization. The restrict keyword can be used instead to allow the implementation to reason about lvalues not being aliased. It seems that the above cannot be the rationale.

Kaz
  • 3,692

6 Answers6

1

The volatile keyword means a lot of different things on a lot of different compilers, but one of their more important uses has been to represent special memory-mapped hardware. So declaring the behavior in that respect undefined allows compilers to keep using it to represent that hardware. Same reason right-shift, divide and modulus of a negative signed integer is not specified exactly: that lets each CPU’s native instruction set work. If you want exactly-defined behavior, there are atomics and div().

Davislor
  • 1,563
1

If you are only interested in "portable C," then you will find the keyword volatile to serve little to no use, and thus the requirements will seem unusual. It's entire purpose in the language is to permit compilers to expose memory mapped hardware. It gets some minimal life if you add a [non-portable] threading library into the mix, but at its core, it's job is to do implementation specific behaviors. So much so that the spec states:

An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. ... What constitutes an access to an object that has volatile-qualified type is implementation-defined. (ISO/IEC 9899:TC2 6.7.3.6)

The value in denying access to a volatile object via non-volatile means becomes apparent in that line. The spec permits "volatile" operations to be implemented differently than "non-volatile" operations. As a trivial example, a volatile write might look up a mapped register in a mapping, and then write to the register instead, while the non-volatile write simply writes to that memory. That example might be a little unrealistic, but the developers of C went to great lengths to support writing software in C on many platforms, and other than adding one line into the spec (the one you mentioned), it literally cost them nothing to add this support to the language. In doing so, all sorts of exotic memory mapping situations are made much simpler.

On modern x86 machines, there's probably no reason for the compiler to take advantage of this. In fact, I would not be surprised if the x86 compilers "undefined behavior" in this case is to just do what you think should have been done. However, on more anemic platforms, the ability to not have to worry about the case where a developer access a volatile via a non-volatile lvalue may be a substantial difference in compiler code generation.

Cort Ammon
  • 11,917
  • 3
  • 26
  • 35
1

In a comment to Lorehead, Kaz correctly remarks that this rule only is relevant if the code is otherwise well-defined. You can't make code more Undefined after all, UB is a binary state.

So take the portable uses of volatile, i.e. with signals and longjmp. In both cases the compiler must ensure the proper semantics are being respected. In particular, the update of visible (volatile) global state must be as described in the code. Non-volatile state can be updated in any order.

Now let's assume that a write through int* could affect volatile state. It means any indirect write cannot be reordered anymore. This is a huge performance impact. Even worse, all reads through int* have to be done and cannot be cached in a register.

Thus, this rule allows for significant optimizations, by massively increasing the number of memory operations that can be reordered.

MSalters
  • 9,038
0

Imagine a hypothetical machine with a magic memory location 0xc0103 that, when written to, sets the brightness of a blinkenlight at the machine chassis to the stored value.

You might have a program like this.

volatile uint32_t * pindicator = (volaitle uint32_t *) 0xc0103;

for (;;)
  {
    uint32_t amount = do_some_work();
    *pindicator = amount;
  }

It would make the light flash according to the amount of work currently handled by the system. Even though the program never reads the value of *pindicator, the compiler is not allowed to optimize any store to it away because it is qualified as volatile. The compiler does not know what consequences a store to *pindicator might have but you know and you can write your program accordingly relying on the compiler not getting into your way.

However, if we were to access it via a non-volatile pointer, the compiler would be free to optimize away any store that it can prove has no visible side effect to the program under the as-if rule. In this simple example, the worst this could do is make the light flash in erratic ways or – more likely – stay forever dark. But the C standard doesn't know about blinkenlights and 0xc0103. It cannot say what consequences would be permissible for accessing the variable though a non-volatile pointer. Another variable might refer to a “heartbeat” register that must be incremented at least once per second and failure to do so might cause a safety mechanism to set off a fuse that makes the device explode. So, the standards way of saying “anything or nothing might happen” is to say: “the behavior is undefined”.

5gon12eder
  • 7,236
0

Quite simply, non-volatile access to volatile variables must be either defined or undefined. So if you want to remove the section that makes it undefined, you must make it defined. I'd be curious how you would do that. For example, just define the behaviour of

int* p = "address of variable which may or may not be volatile";
*p = 1;

in a way that is efficient when *p is not volatile, and defined when *p is volatile. And remember that most uses of pointers fall into this category.

gnasher729
  • 49,096
-1

Non volatile access to a variable is faster as this access can be from local cache, but what you read can have changed else where hence is is dangerous to do this unless you know the value does not change during access.

In case there are 2 non volatile modifications how this is resolved is compiler and hardware dependent.

How these kind of modification are hardware dependent hence the most efficient way to handle it is to leave it undefined so the compiler can handle it the best possible way while letting you benefit for non volatile access to even volatile variables when you know what you are doing to reap speed ups.