You should not be setting everything to volatile. Volatile has a specific purpose, and that's to prevent the compiler from optimizing out reads and writes to memory. Let's look at a really simple example.
int regular_sum(int* ptr) {
int a = *ptr;
int b = *ptr;
return a + b;
}
int volatile_sum(int volatile* ptr) {
int a = *ptr;
int b = *ptr;
return a + b;
}
When we look at the assembly, we see that in regular_sum
, the compiler realizes you're dereferencing the same pointer twice, and it optimizes it to just one dereference. But in volatile_sum
, the compiler inserts both dereferences:
regular_sum(int*):
mov eax, DWORD PTR [rdi]
add eax, eax
ret
volatile_sum(int volatile*):
mov eax, DWORD PTR [rdi]
mov edx, DWORD PTR [rdi]
add eax, edx
ret
Optimizations are good, and most of the time, you won't need to use volatile. If you're doing memory-mapped IO, or you're writing values to pins as though they were a pointer, that's where you use volatile. To reiterate what Nathan Oliver said,
You only need to use volatile on variables where the hardware can change the value of the variable because the compiler can't know about that. This is what volatile is for, letting the compiler know this is a special variable that could be changed in a manner it doesn't know about. If the hardware cant change the value on you, then you don't need volatile.
But if you're doing computations on an object, don't use volatile. Do the computations on a normal object, and then copy the result to your volatile pointer.
Volatile and Interrupt Service Routines.
It is appropriate to use volatile
on global variables that might be modified by Interrupt Service Routines. That being said, volatile
cannot be used with objects like std::bitset
because std::bitset
does not have support for volatile operations, and std::bitset
is not trivially copyable.
In this regard, you have two options:
- Use a container that contains volatile primitives (e.g,
std::vector<volatile bool>
- Write your own class, with support for volatile.
If you have a class that is trivially copyable, then you can do something like the following. First, we have to define functions to allow us to copy to and from volatile types:
template<class T>
T volatile_copy(T const volatile& source) {
static_assert(std::is_trivially_copyable_v<T>, "Input must be trivially copyable");
T dest;
auto* dest_ptr = dynamic_cast<char*>(&dest);
auto* source_ptr = dynamic_cast<char const volatile*>(&source);
for(int i = 0; i < sizeof(T); i++) {
dest_ptr[i] = source_ptr[i];
}
return dest;
}
template<class T>
void volatile_assign(T volatile& dest, T const& source) {
static_assert(std::is_trivially_copyable_v<T>, "Input must be trivially copyable");
auto* source_ptr = dynamic_cast<char*>(&source);
auto* dest_ptr = dynamic_cast<char volatile*>(&dest);
for(int i = 0; i < sizeof(T); i++) {
dest_ptr[i] = source_ptr[i];
}
}
Then, we may write a class normally, and as long as it's trivially copyable we can create a copy from a volatile version:
struct MyBitset {
uint64_t bits;
// Logic
void flip() {
bits = ~bits;
}
void addOne() {
bits++;
}
};
volatile MyBitset flags;
void interrupt_handler() {
auto local = volatile_copy(flags);
// Do stuff to local
volatile_assign(flags, local);
};
We can also encapsulate this behavior in a class, so that we can "check out" volatile variables:
template<class T>
struct Checkout {
T local;
T volatile& source;
Checkout(T volatile& source)
: local(volatile_copy(source))
, source(source) {}
void save() {
volatile_assign(source, local);
}
~Checkout() {
save();
}
};
Using this allows us to create local copies of volatile
variables, make modifications to them, and the result will be saved automatically:
volatile MyBitset flags;
void interrupt_handler() {
auto f = Checkout(::flags);
f.local.flip(); //We can call whatever member functions we want on the local
// When the function exits, changes made to the local are automatically assigned to the volatile global
}