Rust optimization of multiple identical function calls or expressions

If f is an immutable function or expression without side effect,
and it is called/used multiple times (with identical values).
Will Rust (or LLVM) optimize it out to calculate f only once?
I tried example 1.
However,
I'm not familiar with Assembly enough to tell whether the compiled code calls f only once.
So I manually optimize the same code in
example 2.
It seems to be that the two examples generate different Assembly code?
Can anyone help on this?

The f function in your code is definitely not pure - it calls thread::sleep.
I think you are confusing purity with immutability - just because a function takes parameters by value or immutable reference doesn't mean it is pure. For example, any function with a println! statement is an impure function, because calling it changes something (in this case, the "state" of stdout).

4 Likes

Thank you for pointing that out. I've no CS or functional programming background. Indeed, I confused myself about purity and immutability. I've update "pure" to "immutable" in my original post.

When you mention "without side effect", it is same as saying "the function is pure".
The function is not pure - it has side effects and so the compiler cannot remove calls to it.

5 Likes

So, in general if a pure function f is called with identical parameters multiple times, the compiler will optimize the code to call f only once right? What is f is a pure expression used at multiple places in the code? Will the compiler also calculate the expression only once?

And what if the expression is something like x - u - v and y - u - v where u and v pure function calls with identical parameters or pure expressions. Will the Rust compiler be smart enough to optimize the code to something equivalent to the logic x - w and y - w where w = u + v?

I'm asking this as I'm writing some performance critical code in Rust and I'd like to know whether I need to manually optimize those situations or not.

It should be able to - this is called, in compiler theory, "common sub-expression elimination (CSE)".
Of course, in an ideal world, the compiler would be able to do a perfect global CSE - but that cannot be done for the interest of compilation speed.
So yes, Rust compiler, afaik, doesn't perform perfect GCSE - but it does have a significant amount of CSE.

2 Likes

Not 100% sure about this, but then again, if you think that calculating (u + v) might be expensive, it is probably a decent idea to manually do the optimization.
But as the old adage goes - premature optimization is the root of all evil.
So the proper process would be not deliberately write in-efficient code and then profile to find areas for improvement.

In my example, u + v is not really expensive but other parts of code are not expensive either. In my situation, it is of significant amount of work to manually optimize common sub-expressions as I have lots of different situations and some situations share some pieces of identical code but not all of them. I was hoping the compiler can do the heavy duty to save manual optimizations.

Thank you for your good suggestions on "no premature optimization" and "profile to improve"!

Unless you are working on very tightly constrained tiny embedded systems, the job that the compiler does will almost always be enough for your purposes. In 2021, it no longer makes sense in most cases to spend time on micro-optimizations.

1 Like

Rust will not guarantee this, though it may happen as an optimization if it's truly pure.

Notably, it almost certainly will not happen in debug mode.

As always for performance, you're better off profiling to find the hot spots, then benchmarking to see if your change is actually better. See Criterion.rs - Criterion.rs Documentation for the usual recommended benchmarking framework.

6 Likes

Not necessarily. If they're floats, it won't optimize anything, because floats are weird due to existence of NaN, Inf, precision guarantees, subnormals, etc.

If overflow checking is enabled, may not optimize it either, because every arithmetic operation gets a hidden if overflow { panic!() }, which is a side effect.

If the values are read from memory, it may not optimize them either, because page faults on memory access are a hidden side effect.

Generally, code has lots of unobvious side effects, and compilers have to be very conservative about preserving weird edge cases that almost nobody cares about.

2 Likes

And that's why by default it's only enabled in debug builds.

It's to expensive to keep it in release versions precisely because it would stop almost all optimizations from happening.

This would preclude so many optimizations it's not even funny. No, mere memory access is not considered a hidden side effect. Unless it's volatile.

The reason sometimes memory accesses are not optimized is the mere fact that LLVM is more tuned for C++ than for Rust. Certain aliasing info based optimizations just simply producing incorrect code and thus are disabled currently.

2 Likes

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.