Volatile Type Qualifier In C/C++

Introduction

The volatile type qualifier is used to indicate that an object can be modified in ways unknown to the compiler. Every access (both read and write) made through an lvalue expression of volatile-qualified type is considered an observable side effect for the purpose of optimization and is evaluated strictly according to the rules of the abstract machine. Therefore, the compiler will only perform very limited optimizations on the code that involves volatile-qualified objects.

In this blog post, we will present a few examples of using the volatile type qualifier in C/C++ programs, examine the assembly code generated by the compiler, and discuss how the volatile type qualifier can be used to disable optimization on purpose.

Non-Volatile Type Qualifier

We will create a simple programs to demonstrate the difference between the assembly code generated by optimization level 0 and 3 for a simple addition program using non-volatile type qualifier.

Adding Two Int Values

This is a simple program that adds two integer values 1 and 2 and stores the result in a third integer variable.

non_volatile_add.cpp
1
2
3
4
5
6
7
8
int main()
{
int a = 1;
int b = 2;
int c = a + b;

return 0;
}

Optimization Level 0

Let’s compile the assembly code for this program with optimization level 0.

1
$ g++ -S -O0 -o non_volatile_add_O0.s non_volatile_add.cpp
non_volatile_add_O0.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
	.file	"non_volatile_add.cpp"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1, -12(%rbp)
movl $2, -8(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:

The key operations of the program are described under the .LFB0 label.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1, -12(%rbp)
movl $2, -8(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc

Specifically, the integer value 1 is loaded to the memory address -12(%rbp) and the integer value 2 is loaded to the memory address -8(%rbp).

1
2
movl	$1, -12(%rbp)
movl $2, -8(%rbp)

Then, the value at the memory address -12(%rbp) is moved to the register %edx and the value at the memory address -8(%rbp) is moved to the register %eax.

1
2
movl	-12(%rbp), %edx
movl -8(%rbp), %eax

Then, the value in the register %edx is added to the value in the register %eax and the result is stored in the register %eax.

1
addl	%edx, %eax

Then, the value in the register %eax is moved to the memory address -4(%rbp).

1
movl	%eax, -4(%rbp)

Finally, the value 0 is moved to the register %eax, which is the register to hold the return value in the x86 calling convention, and the function returns.

1
2
3
4
movl	$0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret

The assembly code generated by optimization level 0 describes exactly what we intend to do in the program. It is straightforward and easy to understand.

Optimization Level 3

Notice that this program does not have any side effects, such as printing the sum of the two integer values. Therefore, the compiler can optimize the program by removing the addition operation and the third integer variable.

Let’s compile the assembly code for this program with optimization level 3.

1
$ g++ -S -O3 -o non_volatile_add_O3.s non_volatile_add.cpp
non_volatile_add_O3.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	.file	"non_volatile_add.cpp"
.text
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:

The key operations described under the the .LFB0 label become much simpler.

1
2
3
4
5
6
.LFB0:
.cfi_startproc
endbr64
xorl %eax, %eax
ret
.cfi_endproc

Because the program does not have any side effects and it only returns 0, the assembly code generated by optimization level 3 is much simpler and more efficient than the one generated by optimization level 0.

1
2
xorl	%eax, %eax
ret

In fact, the assembly code only computes an XOR operation for the value on the register %eax and returns the result (on the register %eax). The result of the XOR operation of two identical values is always 0.

1
2
xorl	%eax, %eax
ret

There is no addition operation and no third integer variable in the assembly code generated by optimization level 3 at all.

Differences Between Optimization Level 0 and 3

The only difference between the assembly code generated by optimization level 0 and 3 is in the .LFB0 label.

Volatile Type Qualifier

We will also create a simple programs to demonstrate the difference between the assembly code generated by optimization level 0 and 3 for a simple addition program using volatile type qualifier and also compare the differences between the assembly code generated by optimization level 0 and 3 for the volatile version of the program with the non-volatile version of the program.

Adding Two Volatile Int Values

This time, let’s add two volatile integer values 1 and 2 and stores the result in an integer variable. Note that volatile integer and integer are actually different types.

volatile_add.cpp
1
2
3
4
5
6
7
8
int main()
{
volatile int a = 1;
volatile int b = 2;
int c = a + b;

return 0;
}

Optimization Level 0

Let’s compile the assembly code for this program with optimization level 0.

1
$ g++ -S -O0 -o volatile_add_O0.s volatile_add.cpp
volatile_add_O0.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
	.file	"volatile_add.cpp"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1, -12(%rbp)
movl $2, -8(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:

Actually, the assembly code generated for this program is exactly the same as the one generated for the non-volatile version of the program.

Optimization Level 3

Let’s compile the assembly code for this program with optimization level 3 to see what changes will be introduced.

1
$ g++ -S -O3 -o volatile_add_O3.s volatile_add.cpp
volatile_add_O3.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	.file	"volatile_add.cpp"
.text
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
movl $1, -8(%rsp)
movl $2, -4(%rsp)
movl -8(%rsp), %eax
movl -4(%rsp), %eax
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:

This time, the key operations described under the the .LFB0 label is simpler than the ones in the non-volatile version of the program at optimization level 0 but is more complicated than the ones in the non-volatile version of the program at optimization level 3.

1
2
3
4
5
6
7
8
9
10
.LFB0:
.cfi_startproc
endbr64
movl $1, -8(%rsp)
movl $2, -4(%rsp)
movl -8(%rsp), %eax
movl -4(%rsp), %eax
xorl %eax, %eax
ret
.cfi_endproc

We could see that two integers 1 and 2 have been loaded to the memory address -8(%rsp) and -4(%rsp) respectively.

1
2
movl	$1, -8(%rsp)
movl $2, -4(%rsp)

Then, the value at the memory address -8(%rsp) is moved to the register %eax and the value at the memory address -4(%rsp) is moved to the register %eax.

1
2
movl	-8(%rsp), %eax
movl -4(%rsp), %eax

Then, the value in the register %eax is XORed with itself and the result is stored in the register %eax.

1
xorl	%eax, %eax

Finally, the value in the register %eax is moved to the register %eax, which is the register to hold the return value in the x86 calling convention, and the function returns.

1
2
movl	%eax, %eax
ret

We could see that it’s actually a combination of the assembly code generated by optimization level 0 and 3 for the non-volatile version of the program. Because the volatile type qualifier is used to inhibit optimization, the assembly code generated by optimization level 3 for the volatile version of the program still performs the integer loading operations, but the addition operation and the third integer variable have been removed.

When to Use Volatile Type Qualifier

The above two examples show that the volatile type qualifier can be used to disable optimization on purpose. But it’s still a little bit unclear when we should use the volatile type qualifier in our programs. In fact, in practice, we actually don’t see the volatile type qualifier used very often because its use cases are very relatively limited.

The volatile type qualifier is usually used to declare variables that can be modified by hardware or other threads. For example, the memory-mapped I/O registers of a microcontroller are usually declared as volatile variables because they can be read and written by the hardware. The volatile type qualifier is also used to declare variables that can be modified by signal handlers. For example, the variables that are modified by the signal handler of the SIGINT signal are usually declared as volatile variables.

volatile_signal.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <csignal>
#include <iostream>

namespace
{
volatile std::sig_atomic_t gSignalStatus;
}

void signal_handler(int signal)
{
gSignalStatus = signal;
std::cout << "SIGINT received." << std::endl;
}

int main()
{
// Install a signal handler
std::signal(SIGINT, signal_handler);
while (gSignalStatus != SIGINT)
{
;
}
}

In the above example, the variable gSignalStatus is declared as a volatile variable because it can be modified by the signal handler of the SIGINT signal. We will send the signal from the terminal to terminate the program by pressing Ctrl + C, and the program is expected to print out the message “SIGINT received.”.

1
2
3
$ g++ volatile_signal.cpp -o volatile_signal -O3
$ ./volatile_signal
^CSIGINT received.

If we remove the volatile type qualifier from the declaration of the variable gSignalStatus, the program will not print out the message “SIGINT received.”.

non_volatile_signal.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <csignal>
#include <iostream>

namespace
{
std::sig_atomic_t gSignalStatus;
}

void signal_handler(int signal)
{
gSignalStatus = signal;
std::cout << "SIGINT received." << std::endl;
}

int main()
{
// Install a signal handler
std::signal(SIGINT, signal_handler);
while (gSignalStatus != SIGINT)
{
;
}
}

The program will optimize out the while loop because the variable gSignalStatus is not declared as a volatile variable. Therefore, the program will not even hang before we press Ctrl + C and print out the message “SIGINT received.”.

1
2
$ g++ non_volatile_signal.cpp -o non_volatile_signal -O3
$ ./non_volatile_signal

References

Author

Lei Mao

Posted on

03-18-2024

Updated on

03-18-2024

Licensed under


Comments