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.
Click here to leave us feedback about this page.