NRVO and RVO

Some of the statements about Rust in this chapter are dependent on the specifics of how the compiler optimizes various programs. Unless otherwise state, the results presented here are based on rustc 1.87 using the 2024 language edition and with -O2 for C++ and --opt-level=2 for Rust.

Unlike C++, Rust does not guarantee return value optimization (RVO). Neither language guarantees named return value optimization (NRVO). However, RVO and NRVO are usually applied in Rust where they would be in C++.

RVO

The pattern where RVO and NRVO are likely most important is in static factory methods (which Rust calls constructor methods). In the following example, C++17 and later guarantee RVO. In Rust the optimization is performed reliably, but is not guaranteed.

struct Widget {
    signed char x;
    double y;
    long z;
};

Widget make(signed char x, double y, long z) {
    return Widget{x, y, z};
}
#![allow(unused)]
fn main() {
struct Widget {
    x: i8,
    y: f64,
    z: i64,
}

impl Widget {
    fn new(x: i8, y: f64, z: i64) -> Self {
        Widget { x, y, z }
    }
}
}

One can see in the assembly that for both programs the value is written directly into the destination provided by the caller.

// C++
make(signed char, double, long):
        mov     BYTE PTR [rdi], sil
        mov     rax, rdi
        mov     QWORD PTR [rdi+16], rdx
        movsd   QWORD PTR [rdi+8], xmm0
        ret
// Rust
new:
        mov     rax, rdi
        mov     byte ptr [rdi + 16], sil
        movsd   qword ptr [rdi], xmm0
        mov     qword ptr [rdi + 8], rdx
        ret

NRVO

NRVO isn't guaranteed in either C++ or Rust, but the optimization often triggers in cases where it is commonly desired. For example, in both C++ and Rust when creating an array, initializing its contents, and then returning it, the initialization assigns directly to the return location.

#include <array>

std::array<int, 10> make() {
    std::array<int, 10> v;
    for (int i = 0; i < 10; i++) {
        v[i] = i;
    }
    return v;
}
#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
fn new() -> [i32; 10] {
    let mut v = [0; 10];
    for i in 0..10 {
        v[i] = i as i32;
    }
    v
}
}

The generated assembly for the two versions of the program are nearly identical, and both construct the array directly in the return location.

// C++
make():
        movdqa  xmm0, XMMWORD PTR .LC0[rip]
        mov     rdx, QWORD PTR .LC2[rip]
        mov     rax, rdi
        movups  XMMWORD PTR [rdi], xmm0
        movdqa  xmm0, XMMWORD PTR .LC1[rip]
        mov     QWORD PTR [rdi+32], rdx
        movups  XMMWORD PTR [rdi+16], xmm0
        ret
.LC0:
        .long   0
        .long   1
        .long   2
        .long   3
.LC1:
        .long   4
        .long   5
        .long   6
        .long   7
.LC2:
        .long   8
        .long   9
// Rust
.LCPI0_0:
        .long   0
        .long   1
        .long   2
        .long   3
.LCPI0_1:
        .long   4
        .long   5
        .long   6
        .long   7
new:
        mov     rax, rdi
        movaps  xmm0, xmmword ptr [rip + .LCPI0_0]
        movups  xmmword ptr [rdi], xmm0
        movaps  xmm0, xmmword ptr [rip + .LCPI0_1]
        movups  xmmword ptr [rdi + 16], xmm0
        movabs  rcx, 38654705672
        mov     qword ptr [rdi + 32], rcx
        ret

Determining whether the optimization occurred

Tools like cargo-show-asm can be used to show the assembly for individual functions in order to confirm that RVO or NRVO was applied where desired.

There are also high-quality benchmarking tools for Rust, which can be used to ensure that changes do not unexpectedly result in worse performance.