Is the optimizing compiler allowed to omit a function call indirectly used in a short-circuit?

Viewed 190

The C and C++ languages evaluate || and && in left-to-right order, and must "short-circuit" the right-hand side if the left-hand side establishes the truth value of the entire expression.

Does either language allow for the generated code to not call foo(), if the result of foo() is stored in a local variable only used on the right-hand-side of a short-circuit operator?

In the example below, the generated code for bar() must emit a call to foo(). When compiling baz(), could a conformant optimizing compiler remove the invocation of foo() since x is known at compile-time?

int foo(int f);

int bar(int x) {
  int const foo_value = foo(x);
  if (x || foo_value) {
    return 123;
  }
  return 456;
}

int baz(/* assume "void" here for C */) {
  return bar(1); // Can this collapse down to "return 123"?
}
3 Answers

Compiler optimizations are not permitted to change the observable behavior of the program.

int foo(int f);

int bar(int x) {
  int const foo_value = foo(x);
  if (x || foo_value) {
    return 123;
  }
  return 456;
}

int baz(/* assume "void" here for C */) {
  return bar(1); // Can this collapse down to "return 123"?
}

Consider foo is (C++):

int foo(int f) {
    std::cout << "hello, foo is called. I am an observable side effect";
    return 42;
}

Then the compiler cannot optimize the call to foo away. The compiler must prove that foo has no side effects. If it cannot do that, bar and in turn foo must be called. Or rather: You cannot tell the difference whether it is called or some optimization is applied, because observable behavior must not change.

Note that certain optimizations are specifically permitted to change observable behavior, for example copy elision. Though this isnt really in contrast to what I said before, because the C++ standard specifies what is allowed and what not, and optimizations may not deviate from that.

Omitting the function call would not be allowed because it would constitute a change in behavior.

Because bar was not declared static, it could be called from a different translation unit. If such code were to call bar(0), then foo has to be called to determine the return value of the function.

In addition, because foo is not defined in this translation unit, the compiler can't make any assumptions about about the function's side effects and therefore can't optimize it away.

Short-circuiting means that the right-hand side operand of the logical operator, and only the right-hand side operand, is not evaluated when its result is not needed. It does not mean that any other expression is not evaluated.

The right-hand side operand of the logical operator is foo_value rather than foo(). These are two very different things.

 foo(); foo(); foo();                     // does whatever foo does exactly three times
 foo_value = foo(); foo_value; foo_value; // does whatever foo does exactly once

Since foo() is not a right-hand side operand of any logical operator, short circuiting has no relevance to its evaluation. The compiler must cause foo to be called, or otherwise cause any side effects that foo might have.

Related