Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why is the optimizer not allowed to fold in "constant context"?

__builtin_is_constant_evaluated is the builtin used to for implementing std::is_constant_evaluated in the standard library on clang and gcc.

code that is not valid in constant context is also often harder for the optimizer to constant fold.

for example:

int f(int i) {
    if (__builtin_is_constant_evaluated())
        return 1;
    else {
        int* ptr = new int(1);
        int i = *ptr;
        delete ptr;
        return i;
    }
}

is emitted by gcc -O3 as:

f(int):
        sub     rsp, 8
        mov     edi, 4
        call    operator new(unsigned long)
        mov     esi, 4
        mov     rdi, rax
        call    operator delete(void*, unsigned long)
        mov     eax, 1
        add     rsp, 8
        ret

so the optimizer used __builtin_is_constant_evaluated() == 0

clang fold this to a constant but this is because clang's optimizer can remove unneeded dynamic allocation, not because it used __builtin_is_constant_evaluated() == 1.

i am aware that this would make the return value of __builtin_is_constant_evaluated() implementation defined because optimization vary from one compiler to an other. but is_constant_evaluated should already be used only when both path have the same observable behaviors.

why does the optimizer not use __builtin_is_constant_evaluated() == 1 and fallback to __builtin_is_constant_evaluated() == 0 if it wasn't able to fold ?

like image 621
Tyker Avatar asked Jun 21 '19 08:06

Tyker


1 Answers

Per [meta.const.eval]:

constexpr bool is_constant_evaluated() noexcept;

Returns: true if and only if evaluation of the call occurs within the evaluation of an expression or conversion that is manifestly constant-evaluated ([expr.const]).

f can never be invoked in a constant-evaluated expression or conversion, so std::is_constant_evaluated() returns false. This is decided by the compiler and has nothing to do with the optimizer.

Of course, if the optimizer can prove that the branches are equivalent, it can do constant fold. But that is optimization after all — beyond the scope of the C++ language itself.

But why is it this way? The proposal that introduced std::is_constant_evaluated is P0595. It explains the idea well:

constexpr double power(double b, int x) {
  if (std::is_constant_evaluated() && x >= 0) {
    // A constant-evaluation context: Use a
    // constexpr-friendly algorithm.
    double r = 1.0, p = b;
    unsigned u = (unsigned)x;
    while (u != 0) {
      if (u & 1) r *= p;
      u /= 2;
      p *= p;
    }
    return r;
  } else {
    // Let the code generator figure it out.
    return std::pow(b, (double)x);
  }
}

// ...
double thousand() {
  return power(10.0, 3);  // (3)
}

[...]

Call (3) is a core constant expression, but an implementation is not required to evaluate it at compile time. We therefore specify that it causes std::is_constant_evaluated() to produce false. It's tempting to leave it unspecified whether true or false is produced in that case, but that raises significant semantic concerns: The answer could then become inconsistent across various stages of the compilation. For example:

int *p, *invalid;
constexpr bool is_valid() {
  return std::is_constant_evaluated() ? true : p != invalid;
}
constexpr int get() { return is_valid() ? *p : abort(); }

This example tries to count on the fact that constexpr evaluation detects undefined behavior to avoid the non-constexpr-friendly call to abort() at compile time. However, if std::is_constant_evaluated() can return true, we now end up with a situation where an important run-time check is by-passed.

like image 192
L. F. Avatar answered Sep 29 '22 10:09

L. F.