9

My understanding of the semantics of volatile in C and C++ is that it turns memory access into (observable) side effects. Whenever reading or writing to a memory mapped file (or shared memory) I would expect the the pointer to be volatile qualified, to indicate that this is in fact I/O. (John Regehr wrote a very good article on the semantics of volatile).

Furthermore, I would expect using functions like memcpy() to access shared memory to be incorrect, since the signature suggests the volatile qualification is cast away, and the memory access not be treated as I/O.

In my mind, this is an argument in favor of std::copy(), where the volatile qualifier won't be cast away, and memory accesses being correctly treated as I/O.

However, my experience of using pointers to volatile objects and std::copy() to access memory mapped files is that it's orders of magnitude slower than just using memcpy(). I am tempted to conclude that perhaps clang and GCC are overly conservative in their treatment of volatile. Is that the case?

What guidance is there for accessing shared memory with regards to volatile, if I want to follow the letter of the standard and have it back the semantics I rely on?


Relevant quote from the standard [intro.execution] §14:

Reading an object designated by a volatile glvalue, modifying an object, calling a library I/O function, 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 (or a subexpression) in general includes both value computations (including determining the identity of an object for glvalue evaluation and fetching a value previously assigned to an object for prvalue evaluation) and initiation of side effects. When a call to a library I/O function returns or an access through a volatile glvalue is evaluated the side effect is considered complete, even though some external actions implied by the call (such as the I/O itself) or by the volatile access may not have completed yet.

Arvid
  • 10,915
  • 1
  • 32
  • 40
  • You may want to add a memory barrier before and after memcpy when dealing with volatile. – knivil Aug 18 '17 at 10:13
  • Clarification please do you mean _"volatile pointers"_ or "non volatile pointers to volatile data" ? – Richard Critten Aug 18 '17 at 10:15
  • memory barriers are for thread synchronization. I would expect to use that if I'm synchronizing access with another process (but even that seems a bit grey area, since I don't believe the abstract machine has the concept of synchronizing with other processes) – Arvid Aug 18 '17 at 10:16
  • clarified "pointer to volatile object" – Arvid Aug 18 '17 at 10:18
  • 1
    You're tagging both C and C++, yet talk about "the standard". – Antti Haapala -- Слава Україні Aug 18 '17 at 10:44
  • @AnttiHaapala fair point. I suppose my personal POV is C++, which is intended be defined identical as C. [dcl.type.cv] http://eel.is/c++draft/dcl.type.cv#6 "In general, the semantics of volatile are intended to be the same in C++ as they are in C." – Arvid Aug 18 '17 at 10:50
  • 1
    @Arvid: you mixed up threads, process, memory access and beliefs. – knivil Aug 18 '17 at 10:50
  • @knivil how so? – Arvid Aug 18 '17 at 10:51
  • As I understand you, you want to write to a certain memory location through memcpy and want to ensure that no store operation is optimized away (volatile). Only stores to the same memory location should be optimized away within the barrier. – knivil Aug 18 '17 at 10:58
  • 1
    @knivil I just want to access files on disk via memory as mapped by mmap. That's it. I don't believe "memory barrier" is a concept C++ (there's launder and std::experimental::barrier). I tagged "language lawyer" to indicate that I'm not primarily interested in how some contemporary compilers generate code on some popular architectures, but rather how to express the intention in a way that's correctly captured and executed by the C++ (or C) abstract machine. For the purposes of reading and writing files via mmap(), multiple loads or stores is not really a concern (given a single std::copy) – Arvid Aug 18 '17 at 11:12
  • Then ... since you tagged it as C++ but not C++11 or higher the answer is simple: it is not in the standard. You have to use `volatile` or use system specific functionality. – knivil Aug 18 '17 at 11:50
  • @knivil are you suggesting that the C++ tag *only* refers to C++98? (that's not my impression). But more importantly, are you suggesting the the latest version of C++ has made any significant updates in this regard? – Arvid Aug 19 '17 at 07:16

3 Answers3

4

My understanding of the semantics of volatile in C and C++ is that it turns memory access into I/O

No it does not do that. All volatile does is to communicate from the programmer to the compiler that a certain memory area can be changed at any time, by "something else".

"Something else" might be a lot of different things. Examples:

  • Memory-mapped hardware register
  • Variable shared with an ISR
  • Variable updated from a callback function
  • Variable shared with another thread or process
  • Memory area updated through DMA

Since the standard (5.1.2.3) guarantees that an access (read/write) to a volatile object may not get optimized away, volatile can also be used to block certain compiler optimizations, which is mostly useful in hardware-related programming.

Whenever reading or writing to a memory mapped file (or shared memory) I would expect the the pointer to be volatile qualified

Not necessarily, no. The nature of the data doesn't matter, only how it is updated.

I would expect using functions like memcpy() to access shared memory to be incorrect

Overall it depends on your definition of "shared memory". This is a problem with your whole question, because you keep talking about "shared memory" which is not a formal, well-defined term. Memory shared with another ISR/thread/process?

Yes, memory shared with another ISR/thread/process might have to be declared as volatile, depending on compiler. But this is only becaue volatile can prevent a compiler from making incorrect assumptions and optimize code accessing such "shared" variables the wrong way. Something which was especially prone to happen on older embedded systems compilers. It shouldn't be necessary on modern hosted system compilers.

volatile does not lead to memory barrier behavior. It does not (necessarily) force expressions to get executed in a certain order.

volatile does certainly not guarantee any form of atomicity. This is why the _Atomic type qualifier was added to the C language.

So back to the copy issue - if the memory area is "shared" between several ISRs/threads/processes, then volatile won't help at all. Instead you need some means of synchronization, such as a mutex, semaphore or critical section.

In my mind, this is an argument in favor of std::copy(), where the volatile qualifier won't be cast away, and memory accesses being correctly treated as I/O.

No, this is just wrong, for the already mentioned reasons.

What guidance is there for accessing shared memory with regards to volatile, if I want to follow the letter of the standard and have it back the semantics I rely on?

Use system-specific API:s to protect the memory access, through mutex/semaphore/critical section.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • you say "No it does not do that", followed by a description of various kinds if I/O (i.e. side-effects of the program). An access not being allowed to be optimized away follows from it being a side effect. My question has nothing to do with synchronizing anything. – Arvid Aug 18 '17 at 12:29
  • perhaps my model of mmap() is incorrect, but from the program's point of view, it looks like memory that can change without the program itself writing to it, right? – Arvid Aug 18 '17 at 12:39
  • @Arvid The term I/O has a very specific meaning in computers, namely things that go in and out of the computer. Out of my examples, only hardware registers and DMA can be a form of I/O. While a side-effect, as formally defined by the C standard, is just a "change in the execution environment", which is an incredibly vague term. – Lundin Aug 18 '17 at 12:42
  • If I copy values into a memory mapped range without volatile qualifiers, what prevents the compiler from optimizing that away as dead stores? One answer may be that the memory is returned to munmap(), but that implies that updates to the underlying file object won't be reflected by the map, which it is. – Arvid Aug 18 '17 at 12:43
  • I see. I recognize that I used the term I/O loosely. what I meant was observable side-effects. basically, the things that C++ promises to do – Arvid Aug 18 '17 at 12:44
  • I actually think the term I/O fits very well. All of your examples are ways for a program to communicate with the "outside world", i.e. outside of the abstract machine. Just like printf(), they are things that must be treated as observable side effects of executing the program. You say the "nature" of the data doesn't matter, just how it's updated. From the program's point of view, the memory from a memory mapped file *is* updated in exactly the manners you describe, behind the program's back – Arvid Aug 19 '17 at 06:58
3

I think that you're overthinking this. I don't see any reason for mmap or equivalent (I'll use the POSIX terminology here) memory to be volatile.

From the point of view of the compiler mmap returns an object that is modified and then given to msync or munmap or the implied unmap during _Exit. Those functions need to be treated as I/O, nothing else.

You could pretty much replace mmap with malloc+read and munmap with write+free and you would get most of the guarantees of when and how I/O is done.

Note that this doesn't even require the data to be fed back to munmap, it was just easier to demonstrate it this way. You can have mmap return a piece of memory and also save it internally in a list, then a function (let's call it msyncall) that doesn't have any arguments that writes out all the memory all calls to mmap previously returned. We can then build from that, saying that any function that performs I/O has an implicit msyncall. We don't need to go that far though. From the point of view of a compiler libc is a black box where some function returned some memory, that memory has to be in sync before any other call into libc because the compiler can't know which bits of memory that were previously returned from libc are still referenced and in active use inside.

The above paragraph is how it works in practice, but how can we approach it from the point of view of a standard? Let's look at a similar problem first. For threads the shared memory is only synchronized at some very specific function calls. This is quite important because modern CPUs reorder reads and writes and memory barriers are expensive and old CPUs could need explicit cache flushes before written data was visible by others (be it other threads, processes or I/O). The specification for mmap says:

The application must ensure correct synchronization when using mmap() in conjunction with any other file access method

but it doesn't specify how that synchronization is done. I know in practice that synchronization pretty much has to be msync because there are still systems out there where read/write are not using the same page cache as mmap.

Art
  • 19,807
  • 1
  • 34
  • 60
  • interesting. This is a good point, from the program's point of view it's ``mmap()`` and ``munmap()`` that performs the I/O, and since the memory returned by ``mmap`` is fed back into ``munmap`` there is data dependency to a "write" operation. However, memory mapped files are updated on-the-fly as well, and that's sometimes a desired property. In that case, the memory access itself is the side-effect. Isn't it? i.e. when another process updates the content of the file – Arvid Aug 18 '17 at 12:35
  • @Arvid updated the answer since I couldn't answer that in the character limit. – Art Aug 18 '17 at 13:08
0

My understanding of the semantics of volatile in C and C++ is that it turns memory access into I/O

Your understanding is wrong. Volatile object is side effect volatile - and its value may be changed by something not visible for the compiler during the compilation

so volatile object has to have a permanent (inside its scope of course) memory storage location, has to be read from it before any use, and saved after every change

See the example: https://godbolt.org/g/1vm1pq

BTW IMO that article is rubbish - it assumes that programmers think that volatile means atomicity and coherency as well, which is not the truth. That article should have a title - "Why my understanding of volatile was wrong, and why I still live in the world of myths"

0___________
  • 60,014
  • 4
  • 34
  • 74
  • are you saying "side effect" is different from "I/O"? I don't follow what you mean by "its value may be changed by something now visible to the compiler during compilation". values known at compile time are constants, right? How are constants relevant here? Do you have a reference to the standard you are referring to? – Arvid Aug 18 '17 at 10:11
  • Did you mean "not visible" instead of "now visible?" – Angew is no longer proud of SO Aug 18 '17 at 10:12
  • Yes - obvious typo – 0___________ Aug 18 '17 at 10:14
  • right, exactly. in other words: *access to the volatile object is I/O*. you may read something that your program did not write there, and what you write there is part of the work your program is meant to perform and may not be treated as redundant. Just like calls to printf() is considered I/O and may not be treated as redundant. I don't think there's a contradiction here. – Arvid Aug 18 '17 at 10:21
  • 3
    memory access is not an I/O and has nothing common. I/O register may have an address mapped in the memory, and then it can be accesses by the memory writes and reads. But I/O != memory in general – 0___________ Aug 18 '17 at 10:22
  • 1
    agree, misunderstanding 'volatile' as something similar to 'atomic' or 'synchonised' is popular. Its only 'please not optimise in registers' – Jacek Cz Aug 18 '17 at 10:26
  • 1
    @PeterJ I don't know which part of that article gave you the impression he suggests that volatile can be used for thread synchronization. Bullet 8 is dedicated to fighting that myth – Arvid Aug 18 '17 at 10:30
  • @PeterJ are you making a distinction between "side effect" and "I/O"? I assume you meant to refer to memory access to a volatile object. Obviously access to any other object is completely contained within the abstract machine and may be transformed into anything with the same outcome. I'm happy to refer to it as "side effect" instead since perhaps I/O is defined as something else in the standard. It's not really addressing the question though. – Arvid Aug 18 '17 at 10:35
  • @PeterJ It's not clear what your godbolt example is meant to illustrate, other than volatile memory accesses to be treated as (observable) side effects of the program – Arvid Aug 18 '17 at 10:37
  • @Jacek Cz it is a bit more. Nothing common with optimisation – 0___________ Aug 18 '17 at 10:48
  • 1
    @PeterJ I see how I misread your criticism of the article now. My experience though is that this is still a surprisingly common misconception still (i.e. thinking volatile is used for thread sync.) I don't think this warrants referring to it as rubbish. – Arvid Aug 18 '17 at 10:58
  • It is an opinion - I can have different than you. – 0___________ Aug 18 '17 at 10:59
  • 3
    Downvoted for unfair and misleading criticism of the linked article. Many people *do* still believe that ``volatile`` can be used for thread-synchronization. The article's author says that this is not valid, so his statements are both valid and relevant. I don't agree with some of the wording, but what you are doing is pure name-calling with no merit. – Arne Vogel Aug 18 '17 at 11:06
  • 3
    This answer should have the title "why my understanding of the linked article is wrong and why I didn't read it in the first place". – Lundin Aug 18 '17 at 11:08
  • 1
    Stop quoting examples from individual compilers in discussions about standards. – Ajay Brahmakshatriya Aug 18 '17 at 11:11
  • @Ajay Brahmakshatriya it is exactly as the standard states. And you stop to caution me. – 0___________ Aug 18 '17 at 11:11
  • 1
    @PeterJ then link the relevant sections from the standards. – Ajay Brahmakshatriya Aug 18 '17 at 11:13
  • Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects,12) which are changes in the state of the execution environment. Evaluation of an expression in general includes both value computations and initiation of side effects. Value computation for an lvalue expression includes determining the identity of the designated object. ....... The side effect of updating the stored value of the left operand is sequenced after the value computations of the left and right operands. – 0___________ Aug 18 '17 at 11:19
  • The evaluations of the operands are unsequenced. – 0___________ Aug 18 '17 at 11:19
  • Whether `volatile` is suitable for thread synchronization depends upon how a particular platform opts to handle it. On a single-core system a `volatile` which is designed to be suitable for thread-synchronization could provide all the functionality needed for such purpose more cheaply than would be possible via any other means that didn't require implementation-specific syntax. It's more fashionable for compilers to favor "optimization" over semantics that would allow efficient code, but such is life. – supercat Sep 29 '17 at 23:00
  • @supercat total rubbish. You completely do not understand what volatile means. volatile meas only that the object is volatile to the side effects - ie has to to be read before any use and saved after any change- but absolutely nothing else. volatile is not suitable for anything else except to be read every time is used and saved every time is changed. No atomicity, to coherency no thread safety. – 0___________ Sep 29 '17 at 23:19
  • @PeterJ_01: The `volatile` qualifier has whatever effects implementations choose to ascribe to it. Some people seem to think that it must have been impossible to implement thread-safe code on compilers which had no memory-ordering syntax besides `volatile`, ignoring the fact that many embedded operating systems have been built on such compilers. – supercat Sep 30 '17 at 00:16
  • @supercat please stop it. First of all - thread safety may implemented but by the **coder** but definitely not the volatile. volatile means only what I wrote. Everything else from your comment is wrong and shows that you just do not understand this topic and believe in the myths. Please do not write about embedded - I am in this business and here is no magic behind the volatile as well – 0___________ Sep 30 '17 at 08:07