Search code examples
performancevariablesoptimizationrustcompiler-construction

Does the Rust compiler automatically remove unnecessary intermediate variables?


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?


Solution

  • 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.