The volatile memory type declaration simply tells the C compiler that the variable can change any time, e.g. by an interrupt handler. So the compiler doesn't optimize the variable usage by keeping it in some register and fetches/stores the current value each time when accessed.
The important bit there is the preservation of the reads and writes to
storage, producing the same externally visible series of storage events rather than merely producing the same
values in storage at the end.
Just for fun I thought that I'd see what LLVM does in the presence of
volatile at different optimisation levels. I stuck with the Intel version in the expectation that more folks can read Intel assembler than can read ARM assembler. I did actually run the same with
-arch arm and the optimisations are, unsurprisingly, exactly the same logically speaking. The only significant difference with the ARM code is that there are many more loads and stores, which you'd expect from a risc architecture versus a cisc architecture with 'add to memory' instructions.
First up, the C code. Nothing clever, just two fixed length loops incrementing global variables one of which is labelled
volatile, one which is not:
int bert;
void fred()
{
for (int i = 0; i < 10; i++)
{
bert++;
}
}
volatile int harry;
void charlie()
{
for (int i = 0; i < 10; i++)
{
harry++;
}
}
I've removed all the cruft, procedure preludes and postludes and the like from the assembly code to make it more legible.
First up, no optimisation. As you can see, both procedures are compiled into identical code:
_fred: ## @fred
movl $0, -4(%rbp)
LBB0_1: ## =>This Inner Loop Header: Depth=1
cmpl $10, -4(%rbp)
jge LBB0_4
## %bb.2: ## in Loop: Header=BB0_1 Depth=1
movq _bert@GOTPCREL(%rip), %rax
movl (%rax), %ecx
addl $1, %ecx
movl %ecx, (%rax)
## %bb.3: ## in Loop: Header=BB0_1 Depth=1
movl -4(%rbp), %eax
addl $1, %eax
movl %eax, -4(%rbp)
jmp LBB0_1
retq
_charlie: ## @charlie
movl $0, -4(%rbp)
LBB1_1: ## =>This Inner Loop Header: Depth=1
cmpl $10, -4(%rbp)
jge LBB1_4
## %bb.2: ## in Loop: Header=BB1_1 Depth=1
movq _harry@GOTPCREL(%rip), %rax
movl (%rax), %ecx
addl $1, %ecx
movl %ecx, (%rax)
## %bb.3: ## in Loop: Header=BB1_1 Depth=1
movl -4(%rbp), %eax
addl $1, %eax
movl %eax, -4(%rbp)
jmp LBB1_1
retq
Now with
-O1. For the
non-volatile version it's optimised the loop away entirely and replaced the multiple additions by a single constant addition of 10. For the
volatile it's optimised the loop induction variable but kept individual additions to the
volatile variable.
_fred: ## @fred
movq _bert@GOTPCREL(%rip), %rax
addl $10, (%rax)
retq
_charlie: ## @charlie
movl $10, %eax
movq _harry@GOTPCREL(%rip), %rcx
LBB1_1: ## =>This Inner Loop Header: Depth=1
incl (%rcx)
decl %eax
jne LBB1_1
retq
Now with
-O2. For the
non-volatile version it produces identical code, not surprising as the
-O1 optimisation is, quite literally, as close to optimal as you can get without global optimization (where hopefully the compiler would work out that this loop produces a value that is never used anywhere else and elide all of this). The
volatile version has had the loop unrolled, but is still producing the same set of externally visible actions as the previous two versions.
_fred: ## @fred
movq _bert@GOTPCREL(%rip), %rax
addl $10, (%rax)
retq
## -- End function
_charlie: ## @charlie
movq _harry@GOTPCREL(%rip), %rax
incl (%rax)
incl (%rax)
incl (%rax)
incl (%rax)
incl (%rax)
incl (%rax)
incl (%rax)
incl (%rax)
incl (%rax)
incl (%rax)
retq
Using
-O3 produces exactly the same code as
-O2, so there's nothing new to show. So does
-Ofast.
-Os and
-Oz produce the same code as
-O1