5

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.
gnzlbg
  • 7,135
  • 5
  • 53
  • 106
  • Possible duplicates: http://stackoverflow.com/questions/15232758/detecting-constexpr-with-sfinae and http://stackoverflow.com/questions/13299394/is-is-constexpr-possible-in-c11 – Vittorio Romeo Feb 09 '17 at 09:38
  • @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 Feb 09 '17 at 09:45
  • @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 Feb 09 '17 at 10:03
  • 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 Feb 09 '17 at 11:58
  • "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 Feb 09 '17 at 11:59
  • @JohannesSchaub-litb Thanks for the suggestion. I guess that for this to work the caller would need to call `weird(input, is_context_constexpr(parent_function_args...))` and hope that everything works out fine. I also don't think this approach will work with `operator`s, since one cannot add a boolean parameter to them. I also wonder if there are cases in which, even when the argument are constant expression, the compiler will delay the execution till run-time. – gnzlbg Feb 09 '17 at 12:32
  • @JohannesSchaub-litb "Whatever version the caller of the caller wants it to pick", but the caller doesn't care. Whether a function switches algorithm at compile-time or not is an implementation detail of the function. Exposing this implementation detail to the caller is arguably suboptimal. I don't know if it is possible to solve this without doing so, but that would be preferable. – gnzlbg Feb 09 '17 at 12:37
  • @gnzlbg it's not an implementation detail, but a property of the context. For example in `int a[foo()]`, the context is "const", while in `int a = foo();` the context is "non-const" and in `const int a = foo()` the context is dependent on what the definition of `a` wants it to be - hence, it's the decision of the caller. The body of `foo` can't decide it for all of them, since each context has its own requirements (const, non-const, and choice) – Johannes Schaub - litb Feb 09 '17 at 14:08
  • In the `is_context_constexpr(parent_function_args...)` implies that you think that the constexpr context depends on function arguments. Take `int a = foo(1)`. Here, there's no constexpr evaluation, but the arguments are still constant. *However*, if this definition happens inside of a `constexpr` function, this may be within a constexpr evaluation.. but whether that is true does not depend on the function's argument, but on the caller's context. This information can perfectly delivered by the boolean, since the caller is aware either directly by context or indirectly by the boolean parameter. – Johannes Schaub - litb Feb 09 '17 at 14:09
  • @JohannesSchaub-litb "this may be within a constexpr evaluation.. " hence the real question should be "how can I detect whether the current function is being evaluated in a constexpr context"? D has a __ctfe builtin that detects constexpr evaluation and can be used to branch on that. Chandler Carruth and Richard Smith agreed that a function attribute `[[constexpr_alias(constexpr_function_name)]] function-decl;` would be a good way to fix this, by providing a constexpr function associated with a run-time function to be used on constexpr contexts. – gnzlbg Feb 09 '17 at 14:35
  • You can theoretically write two version of SIMD operations, which are chosen depending on ifdef. First compile your code without SIMD support, so that constexpr works well. Then remove the symbols which you would like to optimize. Lastly, compile some part of your code with SIMD support, and link everything together. Perhaps it can be implemented in some relatively clear way, but the idea is to strip/remove/hide scalar versions from object file, and build simd versions separately. – stgatilov Feb 10 '17 at 16:16
  • @gnzlbg, I'm not sure if you still care about this, but if so I'm considering adding something that could help [to SIMDe](https://github.com/nemequ/simde/issues/48), you may want to weigh in. – nemequ Nov 27 '19 at 22:04
  • You can already use `if (std::is_constant_evaluated()) { generic software impl } else { ...arch-specific impl using SIMD... }` to solve this question. – gnzlbg Nov 29 '19 at 09:50

1 Answers1

4

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.

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • I am marking it as accepted since there is probably no other way to do anything like this in standard C++. Might be worth it to remark that this solution does not work with operators, some special member functions, etc. – gnzlbg Feb 09 '17 at 14:38