Volatile in C and Compiler optimization

Volatile is a keyword in C language that explicitly tells the compiler that this variable can be modified from outside of program scope and thus prevents the compiler from optimizing that variable.

The first question is who can modify it — the answer is: by the operating system, by another thread of execution such as an interrupt routine or signal handler, or by hardware — anyone.

Now another question: what kind of optimization?

For example, whenever a variable is used frequently this leads to frequent read/write operations at that memory location, resulting in lower performance due to time cost. In such cases the compiler caches its value in a register to avoid memory access and improve performance.

This is a good approach for higher performance, but the problem is when another program/OS wants to access the fresh value from that memory location, they will never get it because the compiler has optimized that variable and placed it in a register.

Therefore declaring a variable as volatile tells the compiler that a specific type of behavior is intended, and that such code must not be optimized in such a way that it removes the intended functionality.

Let's see the hidden picture:

//Nonvolatile version of buffer loop
int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}
//Volatile version of buffer loop
volatile int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}

The use of the volatile keyword is illustrated in the two sample routines above. Both of these routines loop reading a buffer until a status flag buffer_full is true.

Their machine code is produced by the compiler for each of the samples, where the C code for each has been compiled using Optimization level two (O2).

//Nonvolatile version — ARM assembly
read_stream PROC
    LDR      r1, |L1.28|
    MOV      r0, #0
    LDR      r1, [r1, #0]
|L1.12|
    CMP      r1, #0
    ADDEQ    r0, r0, #1
    BEQ      |L1.12|      ; infinite loop
    BX       lr
    ENDP
|L1.28|
    DCD      ||.data||
    AREA ||.data||, DATA, ALIGN=2
buffer_full
    DCD      0x00000000

//Volatile version — ARM assembly
read_stream PROC
    LDR      r1, |L1.28|
    MOV      r0, #0
|L1.8|
    LDR      r2, [r1, #0]  ; buffer_full — reloaded each iteration
    CMP      r2, #0
    ADDEQ    r0, r0, #1
    BEQ      |L1.8|
    BX       lr
    ENDP
|L1.28|
    DCD      ||.data||
    AREA ||.data||, DATA, ALIGN=2
buffer_full
    DCD      0x00000000

In the disassembly of the nonvolatile form, LDR r0, [r0, #0] loads the value of buffer_full into register r0 outside the loop labeled |L1.12|. Because buffer_full is not declared as volatile, the compiler assumes that its value cannot be modified outside the program. Having already read the value of buffer_full into r0, the compiler omits to reload the variable when optimizations are enabled. The result is an infinite loop.

In the volatile form, the compiler assumes the value of buffer_full can change outside the program and performs no optimizations — the value of buffer_full is loaded into the register r0 inside the loop labeled |L1.8| each time.

In coding practice, we must declare a variable as volatile in the following cases:

The compiler will never optimize the variables you have declared as volatile.