When *writing* to objects on the stack that are never *read* afterwards before they get out of scope, the compiler is free to optimize out those writes entirely. From a purely functional POV, those writes would have absolutely ZERO effect. The compiler assumes that the local stack is under its full control - so in some cases, even with a volatile qualifier, the code may be optimized out if the local variables in question are never read
That's not right: the purpose of volatile is to define what is observable behaviour of the program: the concept of a stack is implementation, it doesn't matter what the scope of the variable is, if it's volatile all accesses to it have to made strictly per the semantics of C. As far as C language is concerned, there is no concept of stack just objects that can be accessed (read or write). Although its implementation defined what it means for an access to be 'obervable', it cannot 'optimise away' a volatile access, be declaring it volatile you are telling the compiler it is observable.
Sorry, but... no.... on almost all points you make here, except for the stack. The standard doesn't even talk about stacks, and any implementation is indeed free to implement local variables (and "return addresses") any way it sees fit. It just so happens that the most common way, by far, is to use stacks (the only C compiler that I vaguely remember of that didn't even use a stack was an old one for a tiny programmable chip, and it was not even really C-compliant anyway), which is why I used this as an example. But yeah, let's remove the "stack" term here, no problem with that. It was too specific.
The idea still remains: any *local* object that is not qualified static ceases to exist once it gets out of scope, so anything happening on such an object AFTER it goes out of scope just doesn't exist per the definition, wherever this object is actually stored (stack, registers, or whatever else the implementation does.)
As to volatile, it's - unfortunately - more subtle than what you said.
1. To begin with: you talk about "observable". The only sentence actually using this term (in C99 at least) in the std is in this part:
An object that is accessed through a restrict-qualified pointer has a special association
with that pointer. This association, defined in 6.7.3.1 below, requires that all accesses to
that object use, directly or indirectly, the value of that particular pointer.
The intended
use of the restrict qualifier (like the register storage class) is to promote
optimization, and deleting all instances of the qualifier from all preprocessing translation
units composing a conforming program does not change its meaning (i.e., observable
behavior).
From what I understand here, this defines an "observable behavior" as the *meaning* of a program. Problem here is: what is the meaning of a program? The way I get this is the same as what I meant by the "functional POV", so anything volatile-related, when it may have unknown side-effects, but no analyzable effect, is NOT observable behavior. I may be wrong here and I admit we are really nitpicking on terms. I could not find the definition of the "meaning of a program" in the std.
For instance, taking the typical "delay loop" example, is a "delay loop", doing absolutely nothing apart from taking CPU cycles, part of the meaning of the program? If you can answer this one by a resounding "yes", without a blink, and backing it up with solid arguments, you are better than I am.
2. More importantly, about the volatile qualifier: it's unfortunately more subtle than it looks. Let's again quote C99 for the relevant parts:
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.
So far so good. Looks like "volatile" will guarantee that such a qualified object is evaluated in all cases, right?
But we need to refer to the "rules of the abstract machine" it mentions. So, again, relevant parts:
Accessing a volatile object, modifying an object, modifying a file, or calling a function
that does any of those operations are all side effects,
which are changes in the state of
the execution environment. Evaluation of an expression may produce side effects. 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. (A summary of the sequence points is given in annex C.)
Still looks, at this point, like the volatile object will be evaluated no matter what. But the following paragraph kind of ruins it all:
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).
So, it looks a bit like what I said earlier. Doesn't it? (See the part in bold.)