7.3 Effect of the volatile keyword on compiler optimization

Use the volatile keyword when declaring variables that the compiler must not optimize. If you do not use the volatile keyword where it is needed, then the compiler might optimize accesses to the variable and generate unintended code or remove intended functionality.

What volatile means

The declaration of a variable as volatile tells the compiler that the variable can be modified at any time externally to the implementation, for example:

  • By the operating system.
  • By another thread of execution such as an interrupt routine or signal handler.
  • By hardware.

This ensures that the compiler does not optimize any use of the variable on the assumption that this variable is unused or unmodified.

When to use volatile

Use the volatile keyword for variables that might be modified external to the implementation.

For example, a variable in a function might be updated by an external process. But if the variable appears unmodified, then the compiler might use the older variable value saved in a register rather than accessing it from memory. Declaring the variable as volatile makes the compiler access this variable from memory whenever the variable is referenced in code. This ensures that the code always uses the updated variable value from memory.

Another example is that a variable might be used to implement a sleep or timer delay. If the variable appears unused, the compiler might remove the timer delay code, unless the variable is declared as volatile.

In practice, you must declare a variable as volatile when:

  • Accessing memory-mapped peripherals.

  • Sharing global variables between multiple threads.

  • Accessing global variables in an interrupt routine or signal handler.

Potential problems when not using volatile

When a volatile variable is not declared as volatile, the compiler assumes that its value cannot be modified externally to the implementation. Therefore, the compiler might perform unwanted optimizations. This can manifest itself in a number of ways:

  • Code might become stuck in a loop while polling hardware.
  • Multi-threaded code might exhibit strange behavior.
  • Optimization might result in the removal of code that implements deliberate timing delays.

Example of infinite loop when not using the volatile keyword

The use of the volatile keyword is illustrated in the two example routines in the following table.

Table 7-5 C code for nonvolatile and volatile buffer loops

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

Both of these routines increment a counter in a loop until a status flag buffer_full is set to true. The state of buffer_full can change asynchronously with program flow.

The example on the left does not declare the variable buffer_full as volatile and is therefore wrong. The example on the right does declare the variable buffer_full as volatile.

The following table shows the corresponding disassembly of the machine code produced by the compiler for each of the examples above. The C code for each example has been compiled using armclang --target=arm-arm-none-eabi -march=armv8-a -Os -S.

Table 7-6 Disassembly for nonvolatile and volatile buffer loop

Nonvolatile version of buffer loop Volatile version of buffer loop
read_stream:                            
        movw    r0, :lower16:buffer_full
        movt    r0, :upper16:buffer_full
        ldr     r1, [r0]
        mvn     r0, #0
.LBB0_1:                                
        add     r0, r0, #1
        cmp     r1, #0
        beq     .LBB0_1     ; infinite loop
        bx      lr
read_stream:                            
        movw    r1, :lower16:buffer_full
        mvn     r0, #0
        movt    r1, :upper16:buffer_full
.LBB1_1:                                
        ldr     r2, [r1]     ; buffer_full
        add     r0, r0, #1
        cmp     r2, #0
        beq     .LBB1_1
        bx      lr

In the disassembly of the nonvolatile example, the statement LDR r1, [r0] loads the value of buffer_full into register r1 outside the loop labeled .LBB0_1. 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 reloading the variable when optimizations are enabled, because its value cannot change. The result is the infinite loop labeled .LBB0_1.

In the disassembly of the volatile example, the compiler assumes that the value of buffer_full can change outside the program and performs no optimization. Consequently, the value of buffer_full is loaded into register r2 inside the loop labeled .LBB1_1. As a result, the assembly code generated for loop .LBB1_1 is correct.

Non-ConfidentialPDF file icon PDF versionARM 100066_0608_00_en
Copyright © 2014–2017 ARM Limited or its affiliates. All rights reserved.