Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?
Asked Answered
A

4

29

This'll be a long one, as to contextualise it and provide as much info as I can, I must meander through various links and quotes - as is often the only way once we enter the C/C++ Standard Rabbit Hole. If you have better citations or any other improvements to this post, please let me know. But to summarise up front, you can blame @zwol for me posting this ;-) and the aim is to find the truth from among two propositions:

  • Do the C and (by import; see comments) C++ Standards require that accesses via volatile * or volatile & must refer to an object originally declared volatile in order to have volatile semantics?
  • Or is accessing a non-volatile-qualified object through a volatile pointer/reference sufficient/supposed to make said accesses behave as if the object was declared volatile?

And either way, if (as it seems) the wording is somewhat ambiguous compared to the intent - can we get that made clear in the Standards themselves?

The 1st of these mutually exclusive interpretations is more commonly held, and that's not entirely without basis. However, I hope to show that there's a significant amount of "reasonable doubt" in favour of the 2nd - especially when we go back to some prior passages in the Rationale and WG Papers.


Accepted wisdom: the referred object itself must have been declared volatile

Yesterday's popular question Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems? arose by assuming a volatile reference would confer volatile behaviour on a non-volatile referent - but finding that it did not, or did to varying degrees and in an unpredictable way.

The accepted answer initially concluded that only the declared type of the referent mattered. This and most comments seemed to agree that equivalent principles are in play as we know well for const: the behaviour would only be volatile (or defined at all) if the reference has the same cv-qualification as the referred object:

The key word in that passage is object. volatile sig_atomic_t flag; is a volatile object. *(volatile char *)foo is merely an access through a volatile-qualified lvalue and the standard does not require that to have any special effects. – zwol

This interpretation appears to be quite widely held, as seen in the responses to this similar-but-hopefully-not-duplicate question: Requirements for behavior of pointer-to-volatile pointing to non-volatile object But there's uncertainty even there: right after the answer says 'no', it then says 'maybe'! Anyway...let's check the Standard to see what the 'no's are based on.


What the Standard says... or doesn't

C11, N1548, §6.7.3: Whereas it's clear that it's UB to access an object defined with volatile or const type via a pointer that doesn't share said qualifier...

6 If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined. 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)

...the Standard doesn't seem to explicitly mention the opposite scenario, namely for volatile. Moreover, when summarising volatile and operations thereon, it now talks about an object that has volatile-qualified type:

7 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. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously.(134) What constitutes an access to an object that has volatile-qualified type is implementation-defined.

Are we to assume "has" is equivalent to "was defined with"? or can "has" refer to a combination of object and reference qualifiers?

A commenter summed up the issue with this sort of wording well:

From n1548 §6.7.3 ¶6 the standard uses the phrase "object defined with a volatile-qualified type" to distinguish it from "lvalue with volatile-qualified type". It's unfortunate that this "object defined with" versus "lvalue" distinction does not carry forward, and the standard then uses "object that has volatile-qualified type", and says that "what constitutes access to an object that has volatile-qualified type is implementation-defined" (which could have said "lvalue" or "object defined with" for clarity). Oh well. – Dietrich Epp

Paragraph 4 of the same section seems to be less frequently quoted but might well be relevant, as we'll see in the next section.


Reasonable doubt: Is/Was a volatile pointer/reference intended to confer volatile semantics on its dereference?

The aforementioned answer has a comment wherein the author cites an earlier statement by the Committee that cast doubt on the 'reference must match referent' idea:

Interestingly, there is one sentence in there [C99 Rationale for volatile] that implies that the committee meant for *(volatile T*)x to force that one access to x to be treated as volatile; but the actual wording of the standard does not achieve this. – zwol

We can find a bit more info on this bit of the Rationale, from the 2nd aforementioned thread: Requirements for behavior of pointer-to-volatile pointing to non-volatile object

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.

philipxy

And from that Bytes thread, we're referred to C99 s6.7.3 p3 - a.k.a. C11's p4 - and this analysis:

The paragraph in question is just before section 6.7.3.1 in the rationale document. If you also need to quote from the standard document itself, cite 6.7.3 p3:

The properties associated with qualified types are meaningful only for expressions that are lvalues.

The expression (volatile WHATEVER) non_volatile_object_identifier is not an lvalue, hence the 'volatile' qualifier is meaningless.

Conversely, the expression * (volatile WHATEVER *) & non_volatile_object_identifier is an lvalue (it may be placed on the left side of an assignment statement), so the property of the 'volatile' qualifier has its intended meaning in this case.

Tim Rentsch

There is a very specific demonstration supporting this idea, with specific regard to the 1st linked question, in WG Paper N1381. This introduced the Annexed memset_s() to do what that OP wanted - guarantee non-elided filling of memory. In discussing possible implementations, it seems to support the idea - by omitting to state any requirement - that using a volatile pointer to alter a non-volatile object should generate code based on the qualifier of the pointer, regardless of that of the referred object...

  1. Platform-independent ' secure-memset' solution:
void *secure_memset(void *v, int c , size_t n) {
    volatile unsigned char *p = v;
    while (n--) *p++ = c;
    return v;
}

This approach will prevent the clearing of memory from being optimized away, and it should work on any standard-compliant platform.

...and that compilers not doing this are on notice...

There has been recent notice that some compilers violate the standard by not always respecting the volatile qualifier.


Who's right?

That was exhausting. There's certainly a lot of room for interpretation here, depending on which documents you happen to have read vs which not, and how you choose to interpret a lot of words that aren't specific enough. It seems clear that something is amiss: either:

  • the Rationale and N1381 are wrongly or haphazardly worded, or
  • they were specifically invalidated retroactively... or
  • the Standard is wrongly or haphazardly worded.

I'd hope that we can do better than all the ambiguity and speculation that seems to have surrounded this in the past - and get a more conclusive statement put on record. To that end, any further sources and thoughts from experts would be very welcome.

Avera answered 7/7, 2016 at 10:47 Comment(27)
I guess the underlying question is "how abstract do we expect the memory model to be?". By qualifying a non-vol pointer as volatile, we seem to be asking the compiler to "write to the I/O / memory directly". That's ok, but if the compiler has previously deduced that the "memory" need not exist, what should it do? Backtrack and create the memory, or ignore you?Ratoon
@downvoters: come on, help me out here.Avera
volatile basically means dealing with unusual memory. How should the compiler decide between unusual memory and normal memory based on pointer type/value.Huonghupeh
"wrongly or haphazardly worded" is it something unusual? Color me cynical.Flyn
@Huonghupeh Good question. There are very valid arguments for not allowing a reference to alter the semantics of its referent. But there are very valid arguments against various things that are unambiguously stated in the Standard, too! I'm asking what the intent is and whether the Standard properly reflects said intent, not for pros and cons either way.Avera
@n.m. I included that specifically because it's very much not unusual, rather unfortunately common. Cynicism won't get us very far, though! As stated, I like to think we can do better. The many past DRs and such attest to this.Avera
You might find this article interesting.Nicety
Not being an expert here, but "volatile semantics" as described in the C++ spec is of multiple faces. Access with volatile lvalues (to not necessarily volatile objects) is always a side-effect. And the other face is that only if you have a volatile object (using a volatile lvalue for access is not enough), the operations you do on it are observable side-effects.Inaccuracy
So even reading through a volatile lvalue constitutes a side effect (not necessarily observable, i.e. may be optimized away), while the same is not true for non-volatile lvalues. So you cannot have two volatilr lvalus reads of the same object without sequencing between them, but that does not imply the side effects induced are observable, unless you deal with real volatile objects (state as of C++11 at least, don't know about C++14!)Inaccuracy
@Nicety Thanks - some light bedtime reading... at a glance, what a mess! I wonder whether they ever found this one: stackoverflow.com/questions/38235112/…Avera
@RichardHodges I deleted my reply to move it elsewhere, thinking your comment was deleted... Again: Your comment raises a very interesting question and is exactly the kind of thing I'm looking for. It's things like that which make me lean, like most, towards the interpretation that references don't have powers to grant volatileity. But an intention that they would was clearly stated in multiple prior documents. So, again, a conclusive citation from the Standard - or correction of it - is the goal. I personally don't mind which interpretation is right. I just want to know what that is.Avera
@Nicety A lot of the points made by that article are right. But a lot of them are legitimate implementation choices misconstrued as violations of the standards by making the incorrect and unsupported assumption that when the standard talks about accesses being observed, it means in the instruction stream. There is no justification for that assumption -- the instruction stream is an internal implementation detail. The standard talks about what the system must do, not what instructions must be used to command it to do things.Aramaic
@DavidSchwartz: For a compiler to be useful for systems programming, unless the same entity controls the compiler and execution platform, the compiler writer must presume that the programmer may knows things about the execution platform that the compiler doesn't. The Standard does not require that compliant implementations be suitable for system programming, but on most platforms any compiler that is suitable for system programming must presume that any volatile-qualified access may be "observable" by mechanisms it knows nothing about.Threap
@Threap Not knowing anything about those mechanisms, they couldn't know what their requirements were. So if it happened to do what those mechanisms required, it would be purely by luck. (In truth, compiler writers have detailed knowledge about the requirements of system developers on the platforms they support. Without it, they would get it wrong as often as they got it right. They still do occasionally get it wrong, of course.)Aramaic
@DavidSchwartz: On most platforms it will be obvious how a compiler can satisfy a programmer's expectations. If a processor has a 32-bit store instruction then given uint32_t volatile *p;, the effect of *p=0x12345678; should be to use a 32-bit store to write 0x12345678 into the address specified by p. If the programmer writes +*p; as a statement, the compiler should use a 32-bit load to read from the address specified by p and ignore the result. The compiler shouldn't care why a programmer would want to do those things--it should simply do them.Threap
@DavidSchwartz: Further, unless looser semantics have been requested, a compiler should ensure that all register-cached values whose address has been exposed to the outside world are synchronized with their underlying storage before and after any volatile access. While this isn't always necessary, it's a safe default. A compiler that wants to allow more efficient code generation can offer a way of waiving such semantics when they're not needed. I'm not sure what's difficult about any of this.Threap
@Threap I don't think that makes any sense. Why should the compiler do that if that's not what the standard requires and not likely to be what the programmer wants? What possible reason could there be? The only plausible reasons are that the standard requires it or that we have platform-specific knowledge that that's helpful. (And no compiler I know of forces such synchronization to memory on x86. It's almost never necessary, and it's absurdly expensive. The standard doesn't require it. It's not helpful. Why do it?)Aramaic
@DavidSchwartz: What do you mean "not likely to be what the programmer wants"? For what other reason might a programmer use the volatile qualifier?Threap
@Threap To communicate with hardware. To synchronize between threads. And lots of other things that aren't sensitive to the order in which writes go to memory. (Either because they don't involve memory at all or because they only access memory through the cache coherency hardware.) That these things are sensitive to the order in which operations go to memory, or that memory barriers actually affect operations to memory on typical hardware, is a common myth. But this is all platform-dependent, of course.Aramaic
@DavidSchwartz: Hardware accesses and thread synchronization would seem sensitive to the order in which the CPU issues loads and stores. I'm not sure what "lots of other things" that aren't sensitive to load/store ordering would be. Whether other actions are required before loads are issued or after stores are issued would typically depend upon hardware configuration, which will often be known by the programmer but not the compiler writer. If it wasn't clear, I'm saying that volatile should ensure that the processor issues writes and reads as indicated; what happens between...Threap
...the local read/write operations and the physical memory is not the compiler's concern, but the compiler shouldn't second-guess the programmer as to whether particular volatile reads or writes need to be issued. Personally I think C would be greatly improved by a "semi-volatile" qualifier which would ensure logical reads and writes would be ordered with regard to "volatile", but not with regard to other "semi-volatile" variables.Threap
@Threap You can say what you think volatile should do based on what would make it useful to people on particular platforms. Just don't pretend that's what the standard says. And don't say that it should be the "order in which the CPU issues loads and stores" as if that weren't a platform-specific thing. (In fact, you can't even define what it means to "issue" a load or store without reference to particular hardware as the boundary between the CPU and memory is squishy and not a hard line like it is in an abstract machine.)Aramaic
@DavidSchwartz: By "order" I mean the logical order in which loads and stores are executed in the code, which is under the control of the compiler. How that translates into physical ordering would often depend upon factors which may be outside the compiler's control but may or may not be outside the programmer's control. If the programmer performs volatile stores in a certain sequence, the compiler should generate code that performs those operations in the same logical sequence.Threap
@DavidSchwartz: In any case, the key point is that a programmer will likely know more about what is required than a compiler will; if a programmer needs some but not all of the operations on an object sequenced with respect to other volatile operations, having a compiler honor volatile will make that possible. If such ordering would be needlessly and unacceptably detrimental to performance, a programmer can use a non-qualified pointer. If, however, the compiler wouldn't honor such ordering and it turned out to be necessary, there may be no efficient way to get needed semantics.Threap
I think it is important to show the gnu.org quote I put in my answer showing a major compiler project's opinion that "it is not clear from the standard whether volatile lvalues provide more guarantees in general than nonvolatile lvalues, if the underlying objects are ordinary." PS Great post. Although I think it is actually another answer to the hopefully-not-duplicate question" I answered. But just what the relationship is between the Qs & As seems as unclear as what their ultimate answer is!Michelsen
See my answer, which addresses a C defect report about the C standard's language vs intent.Michelsen
Don't ask a question for 2 different languages at the same time.Mannered
M
8

Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?

volatile doesn't mean the same thing in C & C++. The C++ standard makes accesses through volatile lvalues observable. [1] It says that it intends this to be the same as C behaviour. And that is the behaviour described in the C Rationale. Nevertheless the C Standard says that accesses to a volatile-declared objects are observable. (Note that accessing a volatile-declared object via a non-volatile lvalue is undefined.)

However. 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).

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

Of course, what is done about observable behaviour is implementation-dependent.

There really isn't any ambiguity. It's just that people can't believe that the C Standard behaviour could be what it is, because that is not the historical usage pre-volatile (when address-literal lvalues were taken to be of volatile objects), as intended by the Rationale, as implemented by compilers before and since, as interpreted and described by the C++ Standard, as corrected in the DR. Similarly, the standard is clear in that it doesn't say that non-volatile accesses are observable, so they're not. (And "side effect" is a term used in defining the evaluation partial order.)

[1] Or at least hopefully it does now. From a comment from underscore_d:

For C++, see also P0612R0: NB comment CH 2: volatile, which was adopted this month to clean up some leftover talk about "volatile objects" in the C++ Standard, when really accesses through volatile glvalues are what it meant (as, presumably/hopefully, what C meant).

Michelsen answered 20/2, 2017 at 2:29 Comment(3)
Excellent! Thank you. I had not checked whether C++ was different, and especially was not aware of that DR for C. I think this is the most accurate answer this question can get. I'll keep an eye on the results of that DR.Avera
For C++, see also P0612R0: NB comment CH 2: volatile, which was adopted this month to clean up some leftover talk about "volatile objects" in the C++ Standard, when really accesses through volatile glvalues are what it meant (as, presumably/hopefully, what C meant).Avera
I edited your latest comment as a footnote in my answer.Michelsen
R
6

converted to answer because I think a thoughtful non-answer may help discover the truth here.

I guess the underlying question is "how abstract do we expect the memory model to be?". By qualifying a non-vol pointer as volatile, we seem to be asking the compiler to "write to the I/O or memory directly". That's ok, but if the compiler has previously deduced that the "memory" need not exist, what should it do? Backtrack and create the memory, or ignore you?

It seems to me that these following two cases are very different in intent:

Memory-Mapped I/O

volatile unsigned char * const uart_base = (volatile unsigned char *)0x10000;

This is clearly intended to inform the compiler that there is a uart memory-mapped at address 0x10000.

Erasing password hashes

void *secure_memset(void *v, int c , size_t n) {
    volatile unsigned char *p = v;
    while (n--) *p++ = c;
    return v;
} 

This is clearly intended to ensure that the memory at v to (int*)v + n is actually modified before the function returns.

However, whether a call to this function could be elided if it was deduced that the memory at v was never needed is unclear.

I would argue that if previously in the program, the memory has been deduced not to need to exist at all, then I would be unsurprised if the call were elided, regardless of the cast to volatile.

Thanks. Because the address is taken, isn't the object required to occupy memory?

gcc seems to agree with you:

#include <cstdint>
#include <cstring>

void * clearmem(void* p, std::size_t len)
{
  auto vp = reinterpret_cast<volatile char*>(p);
  while (len--) {
    *vp++ = 0;
  }
  return p;
}

struct A
{
  char sensitive[100];
  
  A(const char* p)
  {
    std::strcpy(sensitive, p);
  }
  
  ~A() {
    clearmem(&sensitive[0], 100);
  }
};

void use_privacy(A a)
{
  auto b = a;
}


int main()
{
  A a("very private");
  use_privacy(a);
}

yields:

clearmem(void*, unsigned long):
        leaq    (%rdi,%rsi), %rax
        testq   %rsi, %rsi
        je      .L4
.L5:
        movb    $0, (%rdi)
        addq    $1, %rdi
        cmpq    %rax, %rdi
        jne     .L5
.L4:
        xorl    %eax, %eax
        ret
use_privacy(A):
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L10:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L10
        ret
main:
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L13:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L13
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L14:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L14
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L15:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L15
        xorl    %eax, %eax
        ret

clang doesn't elide the construction of the private arrays, so I can't draw any conclusions there.

Ratoon answered 7/7, 2016 at 11:20 Comment(15)
Thanks. Because the address is taken, isn't the object required to occupy memory?Avera
@Avera If that is the case then clang breaks a lot of rules when (not) copying constexpr arrays.Ratoon
@Avera updated answer with some tests. both gcc and clang zero the memory. gcc elides all the copies of character data. clang does not.Ratoon
Very interesting. But which optimisation level did you use? Anyway, perhaps the compiler analyses the function body and uses it to exclude as-if optimisations when it can see the passed address is cast to a volatile reference. This would mean my previous comment wouldn't apply to such cases. Again, this would kinda make sense, but might be very difficult to track and/or hinder other optimisations. The mind bogglesAvera
@underscore_d: I'm not sure I see the difficulty of excluding optimizations when writing to a volatile reference. From the optimizer's perspective, given volatile uint32_t *p; the code *p=6; should be regarded as a call to an opaque function volatile_write32(p,6); until it's time to actually generate the code, whereupon the generated instruction should be a 32-bit store.Threap
@underscore_d: When the expression tree containing the assignment operator is parsed, the compiler should know that the operand of the unary * is a volatile-qualified pointer, and thus that the left-hand operand of the assignment operator is likewise volatile-qualified. At that point, it would seem simple to recognize "assign to volatile lvalue" as an operation equivalent to the aforementioned function call, which would take care of suppressing optimizations no matter where the address came from.Threap
@Threap Sure, I don't see any difficulty whatsoever. That's why I think compilers should do it, and the Standard should make them! But it doesn't seem to enforce this or specify what happens when using a volatile ref to a non-volatile object (only the converse).Avera
@underscore_d: The philosophy of the authors of the Standard is that if there might exist implementations and circumstances where C which might conceivably be more useful if the Standard didn't require them to do something, the Standard should only mandate the behavior if doing so would, on balance, make the language more useful even on those implementations. Since the Standard was never intended to be interpreted as a set of requirements programmers must follow even on platforms where imposing them would serve no useful purpose, there was no "burden of proof" for making something UB.Threap
@Threap Believe me, I get your point, which has been made with great repetition. My point is that not specifying at all what should happen is different from explicitly specifying that it's implementation-defined or UB. You keep talking about the latter, but the Standard seems to have done the former. As far as I can see, the Standard has only qualified as "implementation defined" operations on the vaguely defined "an object that has volatile type" but does not specify whether this includes pointers/references to such or, either way, what should happen to those.Avera
@Avera after reading supercat's message it occurs to me that there could be some architecture or platform where the concept of memory-mapped I/O has no meaning. On such an architecture, the code generator would be within its rights to ignore the volatile keyword altogether, since volatile has no meaning beyond that (it does not, for example force a memory write to be sequenced across threads, so the as-if rule could certainly come into play WRT the interpretation of volatile writes within the same thread).Ratoon
@underscore_d: The Standard generally only labels behaviors as Implementation-Defined if there is no plausible situation or implementation where requiring implementations to commit to a particular behavior would make the language less useful, unless it would on balance make the language more useful even on such platforms. What's needed is a category of behavior where implementations would be very strongly encouraged to commit to a selection of behaviors whenever practical, and would be required to document a refusal to make any commitments in cases where it would be impractical.Threap
@Threap If so, that's another nuance of the Standard's infinite vagueness that I've not absorbed yet! As for the new category, nice idea. Are there any efforts that you know of to introduce such new categories or otherwise improve the ability of the Standard to state itself more clearly and empower compiler writers and users who want more flexibility in such situations? Anyway, you'll need a snappy yet completely unintuitive name for it... implementation-vetoable? I dunno.Avera
@underscore_d: My thought was to define "Testably Constrained Behavior" where implementations would be required to define macros that programs could use to either test supported guarantees or assert requirements, such that implementations that can't support them would be required to refuse compilation, and "Testably-defined behaviors" which would be likewise, but for things which are presently Implementation-Defined. I'd also like to see a concept of a "selectively-conforming" program which would not be required to build on every platform, but would be required to guarantee correct...Threap
...operation on every platform where it does build. Presently, the Standard defines only two categories of programs--a "conforming" program which doesn't violate constraints and has at least platform upon which it will work, and a "strictly-conforming program" which stays below recommended implementation limits, and will which will work on any compliant platform where it doesn't fail for reason of implementation limits. The Standard doesn't presently try to guarantee that any program won't fail in arbitrary fashion on any platform, but adding a concept of...Threap
..."selective conformance" should make it practical to write programs that could be guaranteed either work correctly or fail in defined fashion on any compliant platform.Threap
T
2

The Standard makes no attempt to define all of the behaviors that would be necessary in a useful implementation. The rationale explicitly recognizes the possibility that an implementation can be simultaneously conforming and almost totally useless.

The Standard categorizes the semantics of volatile accesses as implementation-defined, and does not in any way, shape, or form require that an implementation must to define them usefully. It would thus not be unreasonable to say that provided documented and actual behaviors agree, implementation of volatile semantics like those of gcc would make an implementation non-conforming, but would merely make it useless for purposes for which it might otherwise have been suitable.

Note that gcc is often used on platforms where it may be possible to configure an arbitrary region of address space behave somewhat like an I/O device and then later behave like ordinary RAM. It may thus be necessary to ensure that certain operations are very precisely sequenced even though the sequencing of most other operations wouldn't matter; requiring that all operations on something must be treated as volatile in order to have any operations thus treated does not seem a good recipe for optimization.

What I find bizarre is that people have become so interested over the last few years in whether the Standard allows compilers to implement useless semantics for some constructs, for the purpose of improving performance of code that doesn't require that constructs be useful, when such an approach would seem inferior in almost every way to implementing useful semantics by default but providing programmers who don't need such semantics to waive them using command line switches or #pragma directives. If a program includes a [hypothetical] #pragma gcc_loose_volatile directive, gcc can do whatever it likes regardless of how one would otherwise interpret the Standard's requirements regarding volatile, and if it doesn't include such a directive, useless semantics would be useless regardless of whether the Standard prohibited them or not.

Threap answered 7/7, 2016 at 20:32 Comment(3)
But my question isn't about what is valid implementation-defined behaviour for volatile accesses, given that, of course, basically anything is. It's about which operations count as volatile accesses - specifically whether volatile-qualified pointers/references can be considered to grant or not grant volatile behaviour to a referred object not originally declared volatile. I don't know how I can make this clearer.Avera
@underscore_d: I am unaware of anything in the Standard that would make gcc's behavior here non-conforming. Indeed, on platforms which do not define any means via which asynchronous signals could be raised, there would be no good reason for the Standard to prohibit such behavior. The authors of the Standard very seldom say that certain behaviors are must be defined on some platforms but need not be defined on others; they are more prone to rely upon implementers' judgment as to which behaviors any given platform should support. Any good compiler for a platform with asynchronous events...Threap
...will regard the target of a volatile-qualified pointer as volatile, absent directives or compiler options authorizing it to do otherwise. That gcc does not do so makes it a bad compiler for use on any platform where one might use asynchronous events.Threap
B
-1

An object is either volatile or not. Referencing an object using a volatile reference will produce code that is correct whether the object is volatile or not.

Bounty answered 7/7, 2016 at 17:43 Comment(13)
So, is that a yes or a no to the question in the title? The question is what is "correct": should a volatile reference produce code as per the declaration of the referent, or should it produce code as if the referent had been declared volatile?Avera
@Avera Neither behavior is prohibited by the standard. So portable code cannot rely on either. Compilers are free to document their behavior so that platform-specific code can rely on it.Aramaic
@DavidSchwartz You're quite likely right in practice. But the line is "What constitutes an access to an object that has volatile-qualified type is implementation-defined", and my question is whether "an object that has volatile-qualified type" is intended to allow said qualification to be granted by access via a thus-qualified pointer, or whether it just means 'an object originally declared volatile'...and if the latter, why they don't just say so. Trying to interpret Standards is an absurdist challenge. I have nothing but respect for those who manage to build entire compilers upon this messAvera
@Avera Sometimes the best solution is just to avoid trying to language lawyer and just build tools that do what people want them to do. If someone accesses a non-volatile object through a volatile-qualified type, a sensible compiler should do what a typical person familiar with the standard would be most likely to want on that platform.Aramaic
@DavidSchwartz: A sufficiently obtuse implementation can be guaranteed to break almost any program, no matter how portable it tries to be. I would suggest that code which relies upon volatile to be implemented in non-obtuse fashion should be considered portable to non-obtuse implementations.Threap
@Threap And I would say the only portable use of volatile is with signals.Aramaic
@DavidSchwartz: C is widely used in embedded systems. Code which interacts with a particular kind of I/O device will obviously only work with devices that behave similarly, but if there are two or more compilers for the same hardware platform [for most platforms there are at least three] there's no good reason code shouldn't be readily portable among them. On most hardware platforms it's pretty clear what volatile should mean, and gcc does not honor the meaning for the hardware platforms it targets.Threap
@Threap I agree, but that's not a standards violation. That's a usability complaint.Aramaic
@underscore_d: With regard to what counts as a "volatile" access, btw, I'd say that if an implementation documents that accesses which are not traceable to objects that lack volatile qualifiers have one effect, and those which are traceable to such objects have another, such behavior may be indistinguishable from that of treating the latter accesses as though they were not volatile qualified, but would still be Standards-compliant.Threap
@Threap True, but again, that only speaks to what some implementation considers defined, not the Standard. I'm only asking about the latter. Your concerns about implementations defining counterintuitive and/or useless things are valid, but I'm not sure they're really relevant.Avera
@underscore_d: Except on platforms where it would be impossible to observe the effects of "volatile", the indicated behavior is either compliant but useless, or non-compliant and useless. This is one of many "holes" in the Standard where implementations aren't required to behave usefully, but quality implementations will do so anyway.Threap
@DavidSchwartz: If a compiler required fifty petabytes of stack space to execute any function whose source text contained the letter "z" and behaved arbitrarily if such space was unavailable, would that be a standards violation or a usability complaint? Under the as-if rule, if a compiler writer that knew there would never be fifty petabytes of stack available simply generated arbitrary code for any function containing the letter "z", would that be a standards violation or a usability complaint? Provided a compiler outputs required diagnostics for ill-formed programs...Threap
...and has sufficient documentation for all "implementation-defined" behaviors, I can't think of any standards violation that couldn't be called a "usability constraint" under the As-If Rule and One Program Rule.Threap

© 2022 - 2024 — McMap. All rights reserved.