Preface
volatile means changeable in Chinese, but this changeable means different from mutable. Mutable refers to compile-time variability. By default, the grammar compiler will not let us modify some variables, but adding mutable lets the compiler know that we have a tough attitude to modify. volatile variability refers to runtime variability, which is a change that compilers cannot perceive, so that compilers do not blindly optimize.
How volatile affects compilation results
Example 1
The compiler optimizes dead code that it deems useless.
int main() { int* reg = 0x123456; // Suppose 0x123456 is the address of a special register *reg = 0x1; *reg = 0x2; *reg = 0x3; *reg = 0x4; return 0; }
After compiling the above code through g++ a.cpp-O2, you will find that only * reg = 0x4; it works, and other statements are optimized.
0x0000000000400540 <+0>: movl $0x4,0x123456 0x000000000040054b <+11>: xor %eax,%eax 0x000000000040054d <+13>: retq
Normally, this doesn't matter, but the code in the example above actually initializes the state of a device, and assignment of any state can't be omitted. Otherwise, it may cause the device to work abnormally, so volatile is needed. When volatile is added, no statement is optimized after compiling through g++ a.cpp-O2 as well.
0x0000000000400540 <+0>: movl $0x1,0x123456 0x000000000040054b <+11>: xor %eax,%eax 0x000000000040054d <+13>: movl $0x2,0x123456 0x0000000000400558 <+24>: movl $0x3,0x123456 0x0000000000400563 <+35>: movl $0x4,0x123456 0x000000000040056e <+46>: retq
Example two
Because registers are fast, compilers use registers to optimize and avoid frequent memory manipulation.
Students who have played MCU or ARM board must have written the horse-running lamp program. The following program can simply simulate the flashing of LED lights.
int main() { int* a = (int*)0x123456; for( int i = 0; i < 1000; ++i ){ *a = ~(*a); // Recurrent reversal, switching on and off LED lights sleep( 1 ); } return 0; }
Compiled by g++ a.cpp-O 2, you will find that the value after each inversion is stored in the register edx, and only the value in the EDX is saved to 0x123456 at the end, which is equivalent to only switching on the LED lamp once, which is not in line with expectations.
0x0000000000400540 <+0>: mov 0x123456,%edx 0x0000000000400547 <+7>: mov $0x3e8,%eax 0x000000000040054c <+12>: nopl 0x0(%rax) 0x0000000000400550 <+16>: sub $0x1,%eax 0x0000000000400553 <+19>: not %edx 0x0000000000400555 <+21>: jne 0x400550 <main+16> 0x0000000000400557 <+23>: mov %edx,0x123456 0x000000000040055e <+30>: xor %eax,%eax 0x0000000000400560 <+32>: retq
When volatile is added, every negative result will be stored in 0x123456, and 1000 times of switching on and off the LED lamp meets the expectation.
=> 0x0000000000400540 <+0>: mov $0x3e8,%edx 0x0000000000400545 <+5>: nopl (%rax) 0x0000000000400548 <+8>: mov 0x123456,%eax 0x000000000040054f <+15>: sub $0x1,%edx 0x0000000000400552 <+18>: not %eax 0x0000000000400554 <+20>: mov %eax,0x123456 # Each negative result is stored in 0x123456 0x000000000040055b <+27>: jne 0x400548 <main+8> 0x000000000040055d <+29>: xor %eax,%eax 0x000000000040055f <+31>: retq
The above example can also be reversed, that is, the program counts the number of times the external switch LED lights are switched (by interrupt sensing), if optimized using registers, the CPU may perceive only one change.
Example three
The compiler can rearrange instructions to facilitate instruction streaming and achieve optimization purposes.
int main() { int* a = (int*)0x123456; // 0x123456 is the address of a device *a = 0x1; // Turn on the device int* b = (int*)0x654321; // 0x654321 is the data reading address of the device printf( "%d\n", *b ); // Read the address's data return 0;; }
The logic of the above code is to read data from the specified address of a device after it is turned on. There is a causal relationship, but the compiler cannot recognize it. So after compiling with g++ a.cpp-O2, you will find that int* b = (int*)0x654321 is ahead of schedule and the program does not meet expectations.
=> 0x0000000000400590 <+0>: sub $0x8,%rsp 0x0000000000400594 <+4>: mov 0x654321,%esi # Read data from 0x654321 first 0x000000000040059b <+11>: movl $0x1,0x123456 # Then set 0x1 to 0x123456 0x00000000004005a6 <+22>: mov $0x400760,%edi 0x00000000004005ab <+27>: xor %eax,%eax 0x00000000004005ad <+29>: callq 0x400530 <printf@plt> 0x00000000004005b2 <+34>: xor %eax,%eax 0x00000000004005b4 <+36>: add $0x8,%rsp 0x00000000004005b8 <+40>: retq
When volatile is used to modify a and b, the order of instruction execution is in line with expectations.
=> 0x0000000000400590 <+0>: sub $0x8,%rsp 0x0000000000400594 <+4>: movl $0x1,0x123456 # First set up 0x000000000040059f <+15>: mov 0x654321,%esi # After read 0x00000000004005a6 <+22>: mov $0x400760,%edi 0x00000000004005ab <+27>: xor %eax,%eax 0x00000000004005ad <+29>: callq 0x400530 <printf@plt> 0x00000000004005b2 <+34>: xor %eax,%eax 0x00000000004005b4 <+36>: add $0x8,%rsp 0x00000000004005b8 <+40>: retq
epilogue
Volatile guarantees that access to the modified variable cannot be optimized within a single thread of execution, and access to the volatile variable of another volatile variable in order or after will not be reordered.
From the above examples, we can see that volatile is commonly used in embedded development, but it is seldom used in general application development. It is generally used with signal processing functions.
volatile does not guarantee multithreading security.
The above code will crash, just look at the meaning.
gcc version 4.8.5 20150623