Adding #[cfg(const)]?

Imagine we have a function which can be const fn. We want this function to be efficient as possible during runtime, so we implement hot parts of the code using SIMD intrinsics or maybe even inline assembly. But it means that our function can not be a const fn anymore, even though we have software fallback, which can run in const contexts. SIMD intrinsics could be eventually constified (though I assume it will be a lot of work), but I doubt we ever get "const assembly".

Currently we have to choose between constification and runtime performance. Note that features will not work well here. If feature enforces software fallback, then we lose runtime performance if any crate in dependency tree uses it. If feature enables more efficient implementation, then code which relies on const fn could fail to compile simply due to the enabled feature.

So would it make sense to introduce something like cfg(const), which would allow us to change codegen for CTFE? Obviously such feature can be abused, so algorithms during CTFE and the usual runtime will be different, but arguably it's not so different from the existing target-based configs.

3 Likes

IIRC @oli-obk had some thoughts on this. (And this seems unrelated to UCG, not sure why you chose that category.)

Yea, the current status on how we can do this is summarized in https://github.com/rust-lang/const-eval/issues/7#issuecomment-576243335 The details may change, but the general system will work this way.

I couldn't find motivation for the intrinsic approach instead of #[cfg(const)]. To me the latter looks like a more natural extension, similar to already existing #[cfg(test)]. And I don't understand the argument about "polluting the const part of a function with the non-const part". If the non-const part of a function is split into a separate function, then it's easy to use #[cfg(const)] on use imports to select an appropriate version, or cfg-if in a more complex scenarios.

Yes, the cfg! macro will not work that well in this scenario, but it could be solved with a const if CONST_EXPR { .. } extension, which could be helpful even on its own.

1 Like

The motivation is that #[cfg(const)] is not possible at all, because that happens at parsing time, and we need to have the const and non-const code available at the same time later in the process.

The polluting part is about permitting both in the same function constexpr style. If we moved to the example shown with if unsafe { is_constant_evaluated() } {, then we would have a MIR body of the function that contains both const evaluable parts and not const evaluable parts. Nothing in our current infrastructure can support this.

The alternative

const fn foo(x: i32) -> i32 {
    if const {
        x * 2
    } else {
        unsafe { bar(x) }
    }
}

means that we'd still have to handle this concept in AST and HIR and then generate two distinct copies of MIR, one for const eval, one for non-const-eval. Whether we do need two distinct copies would then need to be decided by looking at the entire body of the function and looking for if const calls. All this requires infrastructure in parts of the compiler we try to keep simple and without special cases.

The alternative with the intrinsic requires almost no changes to the compiler, except for interpretation of the intrinsic and codegen of the intrinsic. The intrinsic can then be wrapped in arbitrary convenience macros that give users any level of convenience they want. We could then implement a #[const] attribute (in user space!) that works essentially how you are suggesting #[cfg(const)] to work, except that it would generate the two versions of the function and replace the body of the function with a call to the intrinsic

5 Likes

Thank you for the explanation! I thought CTFE handled as a separate target, so I thought the parsing-time solution should be fine.

The proposed user-space #[const] attribute will be a bit less powerful than a hypothetical #[cfg(const)] (e.g. you can't use it on the use statements as in the linked code and mix it with other cfgs), but I guess it could be worked around. And though the intrinsic makes sense from practical point of view, there is still a lingering feeling that it is a yet another stand-alone feature which has to be discovered and learnt, while for a user not familiar with the inner workings of the compiler it could've been a part of the familiar cfg framework. In other words, it looks like a trade-off between simplicity of implementation and minimizing surface area of the language.