Does const fn imply #[inline]?

hello! been using rust since 1.1. first time trying to use const fn. do we want/need #[inline] for cross-crate inlining?

No.

The const keyword has not effect when the function is not used in a const context.

2 Likes

I think that's an interesting question. The purpose of #[inline] is to make sure that the source of a function is available across crates. This is also always the case for generic functions, hence generic functions never need explicit #[inline]. Intuitively, I would assume that in order for const evaluation to work, the source code of a const fn function ought to be available across crates, too; with this in mind it would make a lot of sense if #[inline] becomes unnecessary. (On the other hand, the information necessary for const-evaluating a function vs. the information necesary for inlining a function could be different; the former might not fully include the latter. I suppose whether or not that's the case is the relevant question here.)

In particular, non-#[inline] functions can also become inlinable if the compiler decides so (edit: wait.. I’m not actually 100% certain this is true for inlining across crates); in that sense, the const would indeed have “no effect” in a semantical sense, because whether or not a function is inlinable is an optimization under the scrutiny of the compiler anyways. Following this argument, I would say @alice, that your statement

if it’s true does still not automatically imply that the answer to @Soni’s question is “no”, because the effect that const might have here could still classify as “no effect” under certain interpretations.

2 Likes

so uh given that nobody can quite answer the original question... would it do any harm to just throw #[inline] at it anyway?

The answer to your original question of whether const fn implies #[inline] is no. You can definitely put the annotation on it.

@steffahn Whether or not the function is inlineable is distinct from whether it has an #[inline] annotation - the annotation tells the compiler to be more aggressive in inlining it.

Quoting from https://matklad.github.io/2021/07/09/inline-in-rust.html, #[inline] does appear to be very relevant for the question of whether or not a function is inlinable (across crates!):

Inlining in Rust

In Rust, a unit of (separate) compilation is a crate. If a function f is defined in a crate A, then all calls to f from within A can be inlined, as the compiler has full access to f. If, however, f is called from some downstream crate B, such calls can’t be inlined. B has access only to the signature of f, not its body.

That’s where the main usage of #[inline] comes from — it enables cross-crate inlining. Without #[inline], even the most trivial of functions can’t be inlined across the crate boundary. The benefit is not without a cost — the compiler implements this by compiling a separate copy of the #[inline] function with every crate it is used in, significantly increasing compile times.

Besides #[inline], there are two more exceptions to this. Generic functions are implicitly inlinable. Indeed, the compiler can only compile a generic function when it knows the specific type arguments it is instantiated with. As that is known only in the calling crate, bodies of generic functions have to be always available.

The other exception is link-time optimization. LTO opts out of separate compilation — it makes bodies of all functions available, at the cost of making compilation much slower.

Note in particular the statement, “Without #[inline], even the most trivial of functions can’t be inlined across the crate boundary.”

4 Likes

Right, well, I know that, I just didn't quite mean exactly what I said .. :sweat_smile:

To be more precise, we have two properties (aggressively_inline, inline_cross_crates). Without the annotation, the first half is always false but the second part may be true if the function is generic, or false for non-generic functions. With the annotation, both are always true.

So what I mean is that adding the const keyword does not change either of the two properties, whatever they are.

2 Likes

#[inline] shouldn't aggressively inline. isn't that #[inline(always)]?

but bodies of const fn are available... are they not analogous to generic functions?

I'm pretty sure that both #[inline] and #[inline(always)] tell the compiler to aggressively inline it. The #[inline(always)] annotation is just more aggressive.

I am quite sure that const fn that are not generic will never be inlined cross-crates. (without an #[inline] annotation that is)

1 Like

For me reading this thread, the confusion it gives me is: if the body of a const function from a library crate is not available in a dependent crate, then how can it be used in a const context in that crate (which it appears it can be)? And I guess, if the body is available for const use, then why would it not be treated as any other available/inlinable function body in non-const use?

2 Likes

Of course you can add another parenthetical to that of "(without LTO!)".

The compiler is always allowed to inline something -- even inline(never) can still be inlined. Whether it does is, of course, a much more complicated question, depending on optimization level, code size, LTO, what it's calling, and more.

So as always, the real answer to

is "well, try it and see if materially impacts a metric you care about".

1 Like

In c++ inline doesnt' affect inlining deciiosn. If mere says that the function has external linage and there is a still body provided, but the linker uses weak symbols so it tosses out all the duplicates. c++ has a way to provide functions definitions across compilation units by header files. That is all it does. If you want to force or not force inlining, then there compiler directives for it, Way back when C was created it actally did hint to the compiler that to inline, but it hasnt for along time.

If Rust has reversed this, that would be an enormous negative - people don't make good inlining decisions. I thought rust has a way to get function definitions across crate boundaries by throwing the AST it th crate (used for LTO also)?

None of these answers really make sense.

I think it's worth linking to the reference here:

https://doc.rust-lang.org/reference/attributes/codegen.html#the-inline-attribute

This confirms that all inlining is just a suggestion to the compiler. It doesn't answer the original question, but I think the conclusion above is solid: if you want #[inline] behavior, then you should use the attribute. Using the attribute where it's already implied (it was my understanding this was true for generic functions) isn't a problem, if anything I think it better documents intent.

I'm shocked that inlinie is requiired to get good behavior across crate boundaties. I'm exially shocked the compiler pays attention to it at all for that. This seems like an enormous step backwards in compiler ilining. - we just went back about 15 year I'm going to ask the compiler team if this is true.

Really, this isn't too far from the status quo for C/C++ AFAICT. In a C/C++ program without templates or constexpr, if a function's caller was located in a different source file from its implementation, the compiler would be unable to inline the call. This was the case until the major compilers supported LTO, which still must be enabled manually. This matches with the current behavior in Rust of requiring either #[inline] or lto = "fat" to inline across crates.

3 Likes

Confirmed that with benchmarks/rust-inline at master · matklad/benchmarks · GitHub.

I think it makes sense -- while allegedly const can imply inline, doing that would mean that adding const qualifier would break separate compilation and inflate compile times for the users of the function.

Roughly, the reason why const doesn't imply #[inline] is probably the same reason why #[inline] (or lto) are not default. Namely, that they prevent separate compilation, and that separate compilation is what makes compiling code fast.

8 Likes

const fn does not imply #[inline]. While MIR suitable for CTFE is preserved in the crate metadata for const fn, MIR suitable for codegen is not unless #[inline] is used or the function is generic. The difference between the two is among other things that the former is much less optimized to allow detecting more UB. It also doesn't run various mir passes that are necessary for correct codegen like CriticalCallEdges or StateTransform.

14 Likes

This is exactly the same as how C/C++ don't inline functions defined in other .c/.cpp files -- unless you turn on LTO.

GIving up auto inlining of functions is a horrible trade off. That you have to explicitly label your function inline in Rust when C++ has even left that behind. Seems to be going backwards. People generally make bad optimization decisions and to completely trade them away for compiles times that will struggle regardless is a bad decision.

Not true. Headers provide full source to other compilation units. All those little funcs in the header will be happily inlined. That's one of the reasons c++ is faster. rg, a sort routine that is defined in the header will inline the lambda passed to it.

I'm baffled at this decisions. Inlining is the optimization that makes all other optimizations work. That the compiler can't pick out the inline with functions and export them automatically without me telling it to is so backwards I didn't believe it when I first heard it.