Once you read about the volatile keyword ...
- Tutorial

Today, consider a less exotic scenario for using the volatile keyword .
The C ++ standard defines so-called observable behavior as a sequence of I / O and read / write operations for data declared as volatile (1.9 / 6). Within the scope of preserving the observed behavior, the compiler is allowed to optimize the code as desired.
For example ... Your code allocates memory using the operating system, and you want the operating system to allocate physical memory pages for the entire requested area. Many operating systems allocate pages on the first real use, and this can lead to additional delays, and you, for example, want to avoid these delays and transfer them to an earlier point. You can write code like this:
for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
*ptr;
}
This code runs throughout the area and reads one byte from each page of memory. One problem - the compiler optimizes this code and completely removes it. Has full right - this code does not affect the observed behavior. Your worries about the allocation of pages by the operating system and the delay caused by this do not apply to the observed behavior.
What to do, what to do ... Ah, for sure! Let us forbid the compiler to optimize this code.
#pragma optimize( "", off )
for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
*ptr;
}
#pragma optimize( "", on )
Well, as a result of ...
1. #pragma was used , which makes the code poorly portable, plus ...
2. optimization is turned off completely, and this increases the amount of machine code three times, plus in Visual C ++, for example, this #pragma can only be used outside functions, respectively, do not count on embedding this code in the calling code and further optimization.
The volatile keyword would do a great job here :
for( volatile char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
*ptr;
}
And that's it, exactly the desired effect is achieved - the code instructs the compiler to necessarily read with the given step. Optimization by the compiler is not allowed to change this behavior, because now the sequence of readings refers to the observed behavior.
Now let's try to rewrite the memory in the name of security and paranoia (this is not nonsense, that's how it happens in real life ). In that post, a certain magic function SecureZeroMemory () is mentioned , which supposedly is guaranteed to overwrite the specified memory area with zeros. If you use memset () or an equivalent loop written by yourself, for example, this:
for( size_t index = 0; index < size; index++ )
ptr[index] = 0;
for a local variable, that is, there is a risk that the compiler will delete this loop, because the loop does not affect the observed behavior (the arguments in that post also do not apply to the observed behavior).
So what to do, what to do ... Ah, we “trick” the compiler ... Here's what you can find by the query “prevent memset optimization”:
1. replacing a local variable with a variable in dynamic memory with all the resulting overhead and leakage risk ( message in the archive linux-kernel mailing list )
2. macro with assembler magic ( message in the linux-kernel mailing list archive )
3. offer to use a special preprocessor symbol that prohibits embedding memset ()locally and makes it difficult for the compiler to optimize (of course, this feature should be supported in the version of the library used, plus Visual C ++ 10 can even optimize the code of functions marked as non-embeddable)
4. various read-write sequences using global variables (the code becomes noticeable more and this code is not thread safe)
5. follow reading with an error message if not read the data that has been recorded (the compiler has the right to notice that the "wrong" data okazats can not, and remove this code)
All of these methods have many common features - they are poorly tolerated and difficult to verify. For example, you “tricked” some version of the compiler, and the newer one will have a more intelligent analyzer, which will guess that the code does not make sense, and will delete it, and will not do it everywhere, but only in some places.
You can compile the rewrite function into a separate translation unit so that the compiler does not “see” what it does. After the next change of the compiler, the linker code generation will come into the game (LTCG in Visual C ++, LTO in gcc, or as it is called in the compiler you use) - and the compiler will see clearly and see that rewriting the memory “does not make sense” and will delete it.
No wonder the saying you can't lie to a compiler appeared .
What if you look at a typical implementationSecureZeroMemory () ? It is essentially this:
volatile char *volatilePtr = static_cast(ptr);
for( size_t index; index < size; index++ )
* volatilePtr = 0;
}
EXTREMELY UNEXPECTED ... contrary to all superstitions, the crossed out statement above is incorrect .
In fact - it has. The standard says that the read-write sequence should only be saved for data with the volatile qualifier . Here for such:
volatile buffer[size];
If the data itself does not have a volatile qualifier , and the volatile qualifier is added to a pointer to this data, reading and writing this data no longer applies to the observed behavior:
buffer[size];
SecureZeroMemory(buffer, sizeof(buffer));
All hope for the compiler developers - at the moment, both Visual C ++ and gcc do not optimize memory access through pointers with the volatile qualifier - including because this is one of the important scenarios for using such pointers.
There is no Standard-guaranteed way to overwrite data with a function equivalent to SecureZeroMemory () if a variable with this data does not have a volatile qualifier . In the same way, it is impossible for the code to guarantee memory reading at the very beginning of the post. All possible solutions are not completely portable.
The reason for this is commonplace - it is "not necessary."
Situations when a variable with data to be written goes out of scope, and then the memory it occupies is reused for another variable and read from the new variable without preliminary initialization, refers to undefined behavior. The standard clearly states that in such cases any behavior is acceptable. Usually just read the "garbage" that was recorded in this memory before.
Therefore, from the point of view of the Standard, guaranteed rewriting of such variables before leaving the scope does not make sense. Similarly, it makes no sense to read memory for the sake of reading memory.
Using pointers to volatileis probably the most effective way to solve the problem. First, compiler developers usually knowingly turn off memory access optimization. Secondly, the overhead is minimal. Thirdly, it is relatively easy to check whether this method works or not on a specific implementation - just look at what kind of machine code will be generated for the trivial examples above from this post.
volatile - not only for drivers and operating systems.
Dmitry Meshcheryakov,
product department for developers