For the purpose of debugging, our project is sometimes using global volatile variables:
volatile struct {
uintptr_t last_read = 0;
uintptr_t last_write = 0;
size_t reads = 0;
size_t writes = 0;
} dbg_data = {};
uint64_t register_read(uintptr_t address) {
dbg_data.last_read = address;
dbg_data.reads++;
return *static_cast<volatile uint64_t*>(address);
}
void register_write(uintptr_t address, uint64_t value) {
dbg_data.last_write = address;
dbg_data.writes++;
*static_cast<volatile uint64_t*>(address) = value;
}
They are never read from code, only by debugger to quickly glimpse state of the program. Even though the program is multithreaded, it was decided not to synchronize access to those global variables to avoid performance hit. Even atomics are undesired as they would constantly cause cache invalidation.
The reasoning is that while unsynchronized mix of writes and reads are Undefined Behavior, just writing to the same memory location is only Implementation-Defined Behavior. At worst, a race condition will lead to some values being missed, which is acceptable. Magical thinking about volatile
usually follow.
I am not sure I buy this reasoning, it may be true for the last_read/write
which is only written to, but the counters are read-modify-write, even if the value read is not used anywhere later.
Is the code above Undefined Behavior, or just Implementation Defined one? Can it be solved in a better way without making every field atomic or locking everything with a mutex?
TL;DR: The code has a data race and therefore the behavior is undefined.
More Info:
From [intro.races]:
6.9.2.2 Data races
...
Two expression evaluations conflict if one of them
- modifies ([defns.access]) a memory location ([intro.memory])
...and the other one
- reads or modifies the same memory location
...
(emphasis is mine)
=> So you have data race here due to the writes.
And we also have there:
Any such data race results in undefined behavior.
(emphasis is mine)
=> Your code invokes UB.
In order to fix it you must use some synchronization mechanism (e.g. std::mutex
), or make the data atomic
(in cases where it's applicable).