5
votes

The setup:

I have a function that uses SIMD intrinsics and would like to use it inside some constexpr functions.

For that, I need to make it constexpr. However, the SIMD intrinsics are not marked constexpr, and the constant evaluator of the compiler cannot handle them.

I tried replacing the SIMD intrinsics with a C++ constexpr implementation that does the same thing. The function became 3.5x slower at run-time, but I was able to use it at compile-time (yay?).

The problem:

How can I use this function inside constant expressions without slowing down my program at run-time?

Some ideas:

  • Adding support for constant evaluating all SIMD intrinsics to the compiler constant expression evaluator, for all compilers: probably the right solution, but an impossible titanic task.

More pragmatic solutions would be to either:

  • overload a function depending on whether it is being executed inside a constant expression (that is, provide a constexpr, and a non-constexpr version).
  • or, somehow branch inside a constexpr function between the constexpr and run-time implementation (that is, detect in a branch whether the function is being executed inside a constant expression).

Anyhow, I am open to any suggestion that solves my problem.

Hints:

  • @RMartinhoFernandes suggested in the Lounge to use __builtin_constant_p to detect whether the function arguments are all constant expressions, in which case the compiler would hopefully be at least attempting to evaluate the function at compile-time.

Failed attempts:

  • @Jarod42 made the straight forward suggestion of just using two independent functions. I would briefly like to point out why this cannot work because it is not trivial. This solution assumes that at the call-site it is known whether the function will be constexpr evaluated or not. But this is not the case. Consider a constexpr function calling mine, which version of my function should it pick? It must pick the constexpr one in order for it to compile, but that "outer" constexpr function could still be evaluated at run-time. In that case, it would use the "slow" compile-time implementation, and hence, this approach does not solve the problem.
1
@VittorioRomeo the first link detects whether a function can be evaluated at compile time at all, that is, whether it is "marked" constexpr. The second link detects whether an expression is a constant expression. I don't think those can be used to branch on whether the current execution context is being evaluated at compile-time. But... I haven't really tried. Would like to be proven wrong. - gnzlbg
@VittorioRomeo There was a chat in the Lounge where @RMartinhoFernandes suggested using __builtin_constant_p (from your second link) to detect whether the function arguments are all constant expressions, in which case the compiler would hopefully evaluate the function at compile-time. - gnzlbg
I would use constexpr int weird(int input, bool asconst = false) { return asconst ? slow_but_const(input) : fast_but_nonconst(input); }. Whenever you need the constexpr thing, pass true to asconst. Otherwise, false. If you forget the true, you get a compile time error, because the call will be non-const. So this is safe aswell. - Johannes Schaub - litb
"Consider a constexpr function calling mine, which version of my function should it pick?". The answer would be: Whatever version the caller of the caller wants it to pick. The caller would have the bool asconst parameter aswell, and just pass it along. - Johannes Schaub - litb

1 Answers

4
votes

I would do it like this

constexpr int doit(int input, bool inconst = false) {
   return inconst ? doitconsty(input) : doitfast(input);
}

If the call to doit is inside of a constexpr function that can be called to perform something either at runtime or at compile time, just forward the flag

constexpr int f(int n, bool inconst = false) {
   /* ... */
   int importantInt = doit(n / 42, inconst);
   /* ... */
   return magicResult;
}

Any constexpr evaluation has something where it starts, if I'm not mistaken. Pass the inconst there

enum foo { bar = f(256, true) }

If you are in the runtime world, just call f like anything else

int main() { std::cout << "test-case: " << f(256); }

It should be noted that this does not work for operators, because you can't add the boolean parameter there. Instead, you could pass the value in some different way, if that's fine for you (for primitive values like int and bool, we could not overload the operator either).

template<typename T>
struct maybe_const_value {
   T t;
   bool isconst;
};

enum foo { bar = maybe_const_value{256, true} % magicTransform }; 

int main() { return maybe_const_value{265} % magicTransform; }

The operator function can then check input.isconst and use input.t as the actual value.