Why does compiler not complete const expression evaluation and collapse it entirely

Why does rust not evaluate this function call to a const of 16 and instead performs an actual multiplication godbold example

pub const fn size() -> usize {
    const LEN :usize = 2;
    std::mem::size_of::<u64>() * LEN
}
1 Like

Enable optimization.

6 Likes

It is a surprise to me.
I don't think it is not an optimization problem. It is a const function, which means it should be done in compiling time.

If that is not a problem, that means const fn is useless because for such a simple code, non-const function can be optimized to 16 directly too,

zylthinking@linux:~/code/clickhouse$ cat b.rs
pub fn size() -> usize {
    const LEN: usize = 2;
    std::mem::size_of::<u64>() * LEN
}

pub fn main() {
    let x = size();
    println!("{}", x);
}

zylthinking@linux:~/code/clickhouse$ rustc -O b.rs && objdump -d b | bat

2798   │ 0000000000008920 <_ZN1b4main17hf0eee1d54c1b3d4aE>:                                                                                                                                                                                                              
2799   │     8920:   48 83 ec 48             sub    $0x48,%rsp                                                                                                                                                                                                           
2800   │     8924:   48 c7 04 24 10 00 00    movq   $0x10,(%rsp) // HERE
2801   │     892b:   00 
2802   │     892c:   48 89 e0                mov    %rsp,%rax
2803   │     892f:   48 89 44 24 08          mov    %rax,0x8(%rsp)
2804   │     8934:   48 8d 05 25 52 03 00    lea    0x35225(%rip),%rax        # 3db60 <_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$u64$GT$3fmt17hab738be73c7b7c86E>

[...]

2823   │ 
2824   │ 0000000000008990 <main>:
2825   │     8990:   50                      push   %rax
2826   │     8991:   48 89 f1                mov    %rsi,%rcx
2827   │     8994:   48 63 d7                movslq %edi,%rdx
2828   │     8997:   48 8d 05 82 ff ff ff    lea    -0x7e(%rip),%rax        # 8920 

const fns are required to evaluate to compile-time constant in const context, but they can also be called out of const context, which means they can do calculation at runtime, so code still need to be generated, e.g. for debugging purpose. imagine you step through a const fn from a debugger, you probably want to see the calculation steps.

edit:

when you inspect the generated code, it is by definition out of const context, so what you are seeing is the runtime "backup" implementation, and it should be no difference between a const fn and a non-const fn in this case. so it is actually an optimization problem.

you will not be able to inspect the "generated assmebly for const context", because it doesn't exists: by definition, the value is evaluated down to compile-time constant, it is done in the frontend (at MIR stage I believe, correct me if I was wrong), and the backend isn't involved at all.

9 Likes

Uses of const are more like pasting the expression everywhere they are used. If you want to ensure it is definitely compiled down to a single value, use static.

1 Like

You are completely missing the point of const, then. The goal of const (const items, const functions, const generic parameters) is not optimization.

The point of const is that it guarantees to the compiler that the expression is "simple" enough that it can be evaluated early during compile time, and thus, such values can be used by the type system itself. For example, only a const expression can be used as the size of an array, because the size is part of its type.

This has nothing to do with optimization. Const doesn't help the constant folding pass of an optimizer (this was already a decade-old myth in C and C++); constant folding and constant propagation can be (and is) done purely based on SSA and aliasing information by LLVM, regardless of the compile-time evaluation capabilities of the high-level source language.

4 Likes

I think this is too strong of a statement. What @zylthinking1 wants is also a reasonable thing to have, and assignment of const fn function result to const variable to force compile-time evaluation becomes old quite quickly.

C++20 resolved the issue by adding consteval functions, maybe Rust have to do the same.

No, this is not something that should generally be done, that's exactly my point. Programmers are expected to rely on the optimizer to generate efficient code.

If you need a const fn to be evaluated in a const context, then you are already in a const context, and the compiler will complain if the expression is not const-evaluatable (and will evaluate it if it is). Ie., this should be something you do because the semantics of the code require const-ness, not because you think you can micro-optimize code this way.

1 Like

Except with large and complex enough const functions it's not guaranteed to work. As usual Rice theorem gets us: if constexpr function does lots of calculations (e.g. it traverses const graph of objects to calculate certain properties) then compiler, surprisingly often, couldn't understand that I just want a number and leaves calculations in place.

Then, suddenly, speed of my program crashes and it becomes, literally, 1000 times slower than normal, basically, it's no longer useful.

Worse yet: if someone adds something non-const there and compiler couldn't optimize everything away now. Then I have to track down changes which made everything crazy slow.

consteval serves quite useful need, although, I have to admit, I'm not 100% sure it happens often enough to justify it's addition to the language. Old C++/Rust trick: assign result of const fn to const, then use that const is still available, after all.

Still… C++ did that and it doesn't look like it's too hard to do that thus, maybe rust needs to do the same.

It does help sometimes. Here is a counterexample. sum_without_const_item does not get constant-folded, sum_with_const_item does.

Well, yes. That's a missed optimization. The fact that merely adding a const item can make a difference means that the optimization was possible in the first place (otherwise, adding the const would have been an error). (I guess, ultimately this may boil down to LLVM's heuristic complexity threshold being apparently too low and rustc's unwillingness to unconditionally const-evaluate at the HIR/MIR level unless explicitly asked to?)

Although unfortunate, I don't think this (what basically amounts to a workaround) would warrant a separate language feature in any case.

1 Like

The whole ownership-and-borrow system can be viewed as a workaround for the inability of some people to track pointer liveness.

consteval is not about forcing optimizations. It's about avoiding runtime-dependencies where are not supposed to happen. The side-effect of forcing compile-time evaluation is nice bonus.

I suppose the big difference is advanced support for compile-time programming in C++ vs rudimentary support in Rust, but the idea is the same: ensure that something which programmer assume can be calculated by compiler before running the program can actually be calculated by compiler.

But people are bad at reasoning about big programs. Automated tools such as compilers are much more reliable.

Rust's compile-time programming capabilities are much more sophisticated than those of C++.

1 Like

Which is precisely the issue: what's easy and simple in C++ is very “sophisticated” and hard to use in Rust.

Which means what's done in costexpr functions in C++ is done in macros in Rust. As long as that's the case Rust may live without consteval, I guess.

No, this is not how it works at all.

Then why C++'s non-macro std::cout << std::format("Hello {}!\n", "world"); becomes macro-based print!("Hello {}!\n", "world"); in Rust?

Yes, compile-time checks are added after C++20, but AFAIK in Rust it's still not possible to implement something like print! without macros.

I believe the use of a macro for print! is because it's variadic. You could implement it without macros, but it would just be less ergonomic.

The syntactic things that print! couldn't do if it weren't a macro are:

  • Variadic
  • Referring to variables from within the format string
  • Named arguments
let a = 1;   // variable used by format string
println!(
    "{a} + 1 = {b}",
    b = 2,   // named argument
);

Rust could instead use something more like C++ does (though it would probably not be spelled with the left shift operator — perhaps something "builder"-ish) but this was chosen instead — I imagine because "string with variables interpolated" is a common pattern across languages and generally well-liked.

2 Likes

You wrote many interesting things about print! macro, but entirely ignore the most important part.

The critical advantage of Rust's print! and C++'s std::format over C's printf is static typechecking.

Yet C++ does that with TMP and these are using, as the name implies, templates while Rust can only do that with macros.

This was just an example, though: in C++ you can do lots of things in constexpr functions which are either not possible in Rust's generics or are extremely inconventient in Rust's generics.

This wasn't always the case, though: in C++98 templates were almost as limited as generics in Rust today (GATs were already there, even in C++98, but many other things like variadic templates or if constexpr weren't even imagined in last century).

And maybe, just maybe, 10 or 20 years down the road Rust's metaprogramming will become as flexible as C++ metaprorgamming.

But today… Rust's metaprogramming capabilities are extremely limited and it's hard for me to understand why people are becoming extra-defensive when this obvious deficiency is pointed out.

P.S. Macros in Rust are extra-powerful, compared to C/C++, that's true, but that distinction is much less important. In practice use of source-code generators like lex,yacc,moc and others give you almost all the flexibility that Rust may give you (and more, arguably).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.