In some languages, I'm told that the compiler will perform optimizations that remove unnecessary "intermediate" local variables to improve execution efficiency.
Does somebody know if Rust does this as well? For example, consider the following code snippets:
fn main() {
// has four local variables
let x = 3;
let y = 5;
let temp_result = x + y;
let final_result = temp_result * 40;
println!("The final result is: {}", final_result);
}
Compare to the implementation below, which appears to have zero explicitly created local variables
fn main() {
// has no explicitly created local variables
println!("The final result is: {}", (3+5) * 40);
}
Would these generate identical machine code?
Said differently, does the compiler "realize" that the four local variables in the first implementation are provably equivalent, given the hard-coded integer inputs, to the second implementation?
Here's a playground link to a test version. Looking at the assembly generated in release mode:
playground::main:
pushq %r15
pushq %r14
pushq %r12
pushq %rbx
subq $72, %rsp
####################### f1() here
movl $320, 4(%rsp) # whole function optimized to static value of 320
#######################
leaq 4(%rsp), %r14
movq %r14, 8(%rsp)
movq core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt@GOTPCREL(%rip), %r15
movq %r15, 16(%rsp)
leaq .L__unnamed_2(%rip), %rax
movq %rax, 24(%rsp)
movq $2, 32(%rsp)
movq $0, 40(%rsp)
leaq 8(%rsp), %rbx
movq %rbx, 56(%rsp)
movq $1, 64(%rsp)
movq std::io::stdio::_print@GOTPCREL(%rip), %r12
leaq 24(%rsp), %rdi
callq *%r12
####################### f2() here
movl $320, 4(%rsp) # same as with f1()
#######################
movq %r14, 8(%rsp)
movq %r15, 16(%rsp)
leaq .L__unnamed_3(%rip), %rax
movq %rax, 24(%rsp)
movq $2, 32(%rsp)
movq $0, 40(%rsp)
movq %rbx, 56(%rsp)
movq $1, 64(%rsp)
leaq 24(%rsp), %rdi
callq *%r12
addq $72, %rsp
popq %rbx
popq %r12
popq %r14
popq %r15
retq
Because the return values are known statically in your example, the functions don't even appear in the compiled code. You actually get the same thing even if you define the functions like this:
fn f1(a: i32, b: i32, c:i32) -> i32 {
let x = a;
let y = b;
let temp_result = x + y;
let final_result = temp_result * c;
final_result
}
fn f2(a: i32, b: i32, c: i32) -> i32 {
(a + b) * c
}
pub fn main() {
println!("f1() = {}", f1(3, 5, 40));
println!("f2() = {}", f2(3, 5, 40));
}
What happens if the parameters aren't known at compile time? Here's another playground, this time with the values calculated randomly and with both functions tagged #[inline(never)]
:
playground::f1:
leal (%rdi,%rsi), %eax
imull %edx, %eax
retq
playground::main:
# rng initialization...
.LBB7_20:
movl 8(%rbp,%rax,4), %ebx
addq $1, %rax
movq %rax, (%rbp)
movl %r15d, %edi
movl %r12d, %esi
movl %ebx, %edx
######################## f1() called here
callq playground::f1 #
########################
movl %eax, 4(%rsp)
leaq 4(%rsp), %rax
movq %rax, 8(%rsp)
movq core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt@GOTPCREL(%rip), %r13
movq %r13, 16(%rsp)
leaq .L__unnamed_3(%rip), %rax
movq %rax, 24(%rsp)
movq $2, 32(%rsp)
movq $0, 40(%rsp)
leaq 8(%rsp), %rbp
movq %rbp, 56(%rsp)
movq $1, 64(%rsp)
movq std::io::stdio::_print@GOTPCREL(%rip), %r14
leaq 24(%rsp), %rdi
callq *%r14
movl %r15d, %edi
movl %r12d, %esi
movl %ebx, %edx
######################## and again here!
callq playground::f1 #
########################
# ...
retq
The compiler has actually recognized that the functions are identical and collapsed them into a single definition.