This was inspired by this question/answer and ensuing discussion in the comments: Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems?. Based on others' and my interpretation of what should happening, as discussed in comments, I've submitted it to GCC Bugzilla: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 Other relevant responses are still welcome.
Also, that thread has since given rise to this question: Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?
I know volatile
isn't what most people think it is and is an implementation-defined nest of vipers. And I certainly don't want to use the below constructs in any real code. That said, I'm totally baffled by what's going on in these examples, so I'd really appreciate any elucidation.
My guess is this is due to either highly nuanced interpretation of the Standard or (more likely?) just corner-cases for the optimiser used. Either way, while more academic than practical, I hope this is deemed valuable to analyse, especially given how typically misunderstood volatile
is. Some more data points - or perhaps more likely, points against it - must be good.
Given this code:
#include <cstddef>
void f(void *const p, std::size_t n)
{
unsigned char *y = static_cast<unsigned char *>(p);
volatile unsigned char const x = 42;
// N.B. Yeah, const is weird, but it doesn't change anything
while (n--) {
*y++ = x;
}
}
void g(void *const p, std::size_t n, volatile unsigned char const x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
void h(void *const p, std::size_t n, volatile unsigned char const &x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
int main(int, char **)
{
int y[1000];
f(&y, sizeof y);
volatile unsigned char const x{99};
g(&y, sizeof y, x);
h(&y, sizeof y, x);
}
g++
from gcc (Debian 4.9.2-10) 4.9.2
(Debian stable
a.k.a. Jessie) with the command line g++ -std=c++14 -O3 -S test.cpp
produces the below ASM for main()
. Version Debian 5.4.0-6
(current unstable
) produces equivalent code, but I just happened to run the older one first, so here it is:
main:
.LFB3:
.cfi_startproc
# f()
movb $42, -1(%rsp)
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L21:
subq $1, %rax
movzbl -1(%rsp), %edx
jne .L21
# x = 99
movb $99, -2(%rsp)
movzbl -2(%rsp), %eax
# g()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L22:
subq $1, %rax
jne .L22
# h()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L23:
subq $1, %rax
movzbl -2(%rsp), %edx
jne .L23
# return 0;
xorl %eax, %eax
ret
.cfi_endproc
All 3 functions are inlined, and both that allocate volatile
local variables do so on the stack for fairly obvious reasons. But that's about the only thing they share...
f()
ensures to read from x
on each iteration, presumably due to its volatile
- but just dumps the result to edx
, presumably because the destination y
isn't declared volatile
and is never read, meaning changes to it can be nixed under the as-if rule. OK, makes sense.
volatile
is really for hardware registers, and clearly a local value can't be one of those - and can't otherwise be modified in a volatile
way unless its address is passed out... which it's not. Look, there's just not a lot of sense to be had out of volatile
local values. But C++ lets us declare them and tries to do something with them. And so, confused as always, we stumble onwards.g()
: What. By moving the volatile
source into a pass-by-value parameter, which is still just another local variable, GCC somehow decides it's not or less volatile
, and so it doesn't need to read it every iteration... but it still carries out the loop, despite its body now doing nothing.
h()
: By taking the passed volatile
as pass-by-reference, the same effective behaviour as f()
is restored, so the loop does volatile
reads.
f()
. To elaborate: Imagine x
refers to a hardware register, of which every read has side-effects. You wouldn't want to skip any of those.Adding #define volatile /**/
leads to main()
being a no-op, as you'd expect. So, when present, even on a local variable volatile
does do something... I just have no idea what in the case of g()
. What on Earth is going on there?
volatile
. Neither have an address passed out - and don't have a static
address, ruling out any inline-ASM POKE
ry - so they can never be modified outwith the function. The compiler can see that each is constant, need never be re-read, and volatile
just ain't true -
volatile
) -volatile
local variables more volatile
than others?Is this a weird corner case due to order of optimising analyses or such? As the code is a daft thought-experiment, I wouldn't chastise GCC for this, but it'd be good to know for sure. (Or is g()
the manual timing loop people have dreamt of all these years?) If we conclude there's no Standard bearing on any of this, I'll move it to their Bugzilla just for their information.
And of course, the more important question from a practical perspective, though I don't want that to overshadow the potential for compiler geekery... Which, if any of these, are well-defined/correct according to the Standard?
For f: GCC eliminates the non-volatile stores (but not the loads, which can have side-effects if the source location is a memory mapped hardware register). There is really nothing surprising here.
For g: Because of the x86_64 ABI the parameter x
of g
is allocated in a register (i.e. rdx
) and does not have a location in memory. Reading a general purpose register does not have any observable side effects so the dead read gets eliminted.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With