26

I wrote this code that looked very simple to me at a first glance. It modifies a variable that is referenced by a reference variable and then returns the value of the reference. A simplified version that reproduces the odd behavior looks like this:

#include <iostream>
using std::cout;

struct A {
    int a;
    int& b;

    A(int x) : a(x), b(a) {}
    A(const A& other) : a(other.a), b(a) {}
    A() : a(0), b(a) {}
};

int foo(A a) {
    a.a *= a.b;
    return a.b;
}


int main() {
    A a(3);

    cout << foo(a) << '\n';
    return 0;
}

However, when it is compiled with optimization enabled (g++ 7.5), it produces output different to non-optimized code (i.e. 9 without optimizations - as expected, and 3 with optimizations enabled).

I am aware of the volatile keyword, which prevents the compiler from reordering and other optimizations in the presence of certain side-effects (e.g. async execution and hardware-specific stuff), and it helps in this case as well.

However, I do not understand why I need to declare reference b as volatile in this particular case? Where is the source of error in this code?

Boann
  • 48,794
  • 16
  • 117
  • 146
Vadim Andronov
  • 261
  • 2
  • 5
  • 10
    `volatile` is *very rarely* the solution. Usually it's a bug. – Jesper Juhl Jul 11 '20 at 19:49
  • Doesn’t it reduce to a problem like the following (without classes): int a = 3; int &b = a; a = a * b; - If so, does that fall under the sequence point rule? See https://en.cppreference.com/w/cpp/language/eval_order - “[It is] Undefined Behavior .. if a side effect on a scalar object is *unsequenced relative to another side effect on the same scalar object*..” – user2864740 Jul 11 '20 at 20:06
  • 1
    @AlanBirtles the code in the post reproduces the described problem, and is already short. However, I added missing includes to make it compilable and added emphasis on expected output in the post. – Vadim Andronov Jul 11 '20 at 20:13
  • Managed to reproduce on [godbolt](https://godbolt.org/z/no8dTP). Remove `-O3` to get 9 output. @AlanBirtles – Tony Tannous Jul 11 '20 at 20:19
  • possibly a bug introduced in gcc 6.4 https://godbolt.org/z/nn9G3h? – Alan Birtles Jul 11 '20 at 20:38
  • perhaps the compiler thinks "a.a inside foo isn't used anywhere else, so I can 'optimise' that evaluation away", leaving a.b unchanged. It would be interesting to examine the assembly code to confirm if this is the case – Tasos Papastylianou Jul 11 '20 at 20:49
  • 3
    What the compiler sees is that `a.b` is read twice. When optimizations are enabled (`-O1` is sufficient) this read is optimized down to a single read, so the modified value is not picked in the return statement. I experimented with various `-fno-strict-aliasing`, `-fstrict-aliasing` flags (and the related warnings) but did not find anything that caused the compiler to re-read `a.b`. (gcc 10.1, x64, via Compiler Explorer) – 1201ProgramAlarm Jul 11 '20 at 21:44
  • 5
    This behavior looks like a textbook Undefined Behavior. However the more I look at it, the more it seems to me that it's a gcc bug. Every object is initialized. Afaict there is no dangling reference. If we make everything constexpr clang correctly gives an error "reference to subobject of 'a' is not a constant expression". However gcc gives a wrong error message: "'A(3)' is not a constant expression because it refers to an incompletely initialized variable" which could mean just an bad-worded error text, or it could be that gcc thinks that somehow an subobject is uninitialized here. – bolov Jul 11 '20 at 21:59
  • 1
    Any output other than `9` is a compiler bug – M.M Jul 12 '20 at 03:35
  • 2
    Cases like this where optimization of extremely simple code is screwed up, always leaves me wondering how much critical code is out there that's been mis-compiled and nobody noticed yet – M.M Jul 12 '20 at 03:40
  • 1
    You should file a bug report on GCC [bugzilla](https://gcc.gnu.org/bugzilla/) – Tony Tannous Jul 12 '20 at 05:53

1 Answers1

13

I could not find a source of UB in regard of the standard. This looks to me like a bug of the optimizer that would fail to notice that a.b and a.a both refer to the same object:

  • First of all, foo() works on a copy. I changed foo() to pass by reference, and the expected result was consistently obtained. I suspected an issue in the initialization of the reference. But the provided copy constructor deals correctly with a.b.

  • Then I suspected some UB related to side effects of undeterminately sequenced operations in the same expression. But the side effect on the lhs of *= is sequenced after the rhs, so that there is no UB here either.

  • Adding some logging after the *= statement made it unexpectedly work as expected. This appeared very strange: it looks like the usual problems encountered when strict aliasing constraint is not respected, i.e. when the compiler doesn't realize that a pointed object was modified and otpimizes the code as if the value was unchanged. In such case, it's not unusual that additional code would cause the right value to be reloaded and find a different result.

  • There is however no aliasing issue here, since the original member and the reference to it both are both based on the same type.

When you have eliminated the impossible, whatever remains, however improbable, must be the truth.
- Sir Arthur Conan Doyle

After having eliminated bugs and UB in the OP code, the only remaining possibility is a bug in the optimizer. It seems that the optimizer fails to note that a.a and a.b are the same object, and that it simply reuses the latest known value of a.b which is already in a register.

Christophe
  • 68,716
  • 7
  • 72
  • 138
  • Can you please explain why this answer different from the one I gave? – Coral Kashri Jul 11 '20 at 22:35
  • 3
    @KorelK Your answer cites a compiler bug, but it also invokes a number of misconceptions about how compilers operate (and the role of "reasonable code" vs a standard) and what the expected result of the code should be, which ultimately distract the reader and misinform them. For example, it invokes the notion of updates to memory and the register "racing" each other, which is very likely to be misinterpreted and/or misconstrued. The compiler bug is much likely a simpler aliasing/reuse bug based on invalid assumptions made during optimization. – nanofarad Jul 11 '20 at 22:44
  • @nanofarad I got the same results in C, and in C++ without optimizations. I mentioned there that it is a bug, but I gave the explanation what it might cost to fix it. – Coral Kashri Jul 11 '20 at 22:46
  • 3
    @KorelK I think the reasoning is different: I proceed by eliminating the possible UB cases according to the C++ language specifications, and come to the conclusion of a bug, that I can link to a case where such behavior would be normal but doesn't apply. In your answer you start to look at the generated code, assume this a s a bug, without verifying if there's any UB or implementation dependent rule could explain/allow this code generation. – Christophe Jul 11 '20 at 22:49
  • 4
    @KorelK I won't discount the results of your experiments, but I do disagree with the conclusion that you draw from them. There are plenty of reasons why a function call to `sleep` causes the bug go to away, many of which are far more believable that the compiler somehow emitted assembly that causes a race between stores to registers and mmemory. Furthermore, whether devs "usually" won't update a variable, and return some reference to it is irrelevant. If the C++ spec allows it, then the compiler must support it in order to be standards-compliant. – nanofarad Jul 11 '20 at 22:51
  • @Christophe I agree with your explanation, but I do think this answer deserve at least one comment that says it, rather then kill it with DV lol – Coral Kashri Jul 11 '20 at 22:53
  • I deleted my answer 'cause I am not here to make anyone mistake, but I still believe the the trade-offs that I mentioned there are the reasons the bug exists in the C compiler, and came also to the C++ one. – Coral Kashri Jul 11 '20 at 22:56
  • 3
    @KorelK In my own experience, I must say that I'm always astonished how accurate the optimiser is in its analysis, and how prudent it is when optimisation conditions are not completely guaranteed. I never witnessed any "tradeoff" that would not be allowed according to the language specifications: no compiler vendor would dare to take this risks, because C++ is used in life critical medical devices, nuclear power plants, telecommunication networks, defence systems, and even Space X. – Christophe Jul 11 '20 at 23:19
  • @Christophe I didn't think of it this way, you're right. – Coral Kashri Jul 11 '20 at 23:46
  • @Christophe after talking again with a compilers writer, I understood that I was right about the things I wrote, and compilers optimizations have to take simplifying assumptions, or else most of the optimizations are illegal. – Coral Kashri Jul 12 '20 at 00:49
  • 1
    @KorelK I don't know what you mean by "simplifying assumptions", but the only simplifying assumptions that optimizations can make are "conservative" ones, ie an optimization can be applied in fewer cases than permitted. Your deleted answer isn't consistent with basic principles of programming language definition & implementation. Also it is not clear. – philipxy Jul 14 '20 at 22:48
  • @philipxy Your basic assumption that the compiler is a perfect creature, that can optimize your code using registers while consistently update the RAM, and keep references 100% of the time updated, even when it doesn't fully see the connection between the reference and the source, is wrong. I had this assumption too before, until a compilers writer opened my eyes to the reality. I understand that here you are looking for the rules and not for their reasons, and in the OP question might be a missed rule, but it doesn't mean that these rules have no reasons for existence. – Coral Kashri Jul 15 '20 at 06:46
  • 1
    @KorelK The goal & coding of compiler writers is exactly to make a compiled program act the way the manual says, although there are bugs in compilers, as with any human endeavour. MarkVeltzer's & your comments to the contrary are wrong. (I can't make sense of your last sentence.) We'll have to disagree. https://www.google.com/search?q=site%3Astackoverflow.com+c%2B%2B+what+is+the+as-if+rule – philipxy Jul 15 '20 at 07:44