Rust: enum, boxed error and stack size mystery
Usually you model errors in Rust using enum
and if you want to reduce boilerplate you can use crates like thiserror. Typically you define Error
and ErrorKind
:
use thiserror::Error;
#[derive(Error, Debug)]
#[error(transparent)]
pub struct Error(Box<ErrorKind>);
#[derive(Error, Debug)]
pub enum ErrorKind {
#[error("IllegalFibonacciInputError: {0}")]
IllegalFibonacciInputError(String),
#[error("VeryLargeError:")]
VeryLargeError([i32; 1024])
}
You may notice Error
contains boxed ErrorKind
, the reason is to limit the maximum size of Result<T>
. The size of enum ErrorKind
is equal to the largest variant of the enum + padding, in some cases it can become quite large and method returning a type that contains ErrorKind
will need to use larger stack space even if it is happy path. Here is how memory for enum can be visualized (kudos to Rust container cheat sheet, by Raph Levien)
Let's test that assumption by creating two methods to calculate Fibonacci number recursively with different return types:
pub fn fib1(n: u32) -> Result<u64, Error>
pub fn fib2(n: u32) -> Result<u64, ErrorKind>
First we check what is the size of our types, we can use std::mem::size_of
fn main() {
use std::mem::size_of;
println!("Size of Result<i32, Error>: {}", size_of::<Result<i32, Error>>());
println!("Size of Result<i32, ErrorKind>: {}", size_of::<Result<i32, ErrorKind>>());
}
Produced output make sense (consider padding as well)
Size of Result<i32, Error>: 16
Size of Result<i32, ErrorKind>: 4104
Is it enough? Not really, modern compilers are very complicated systems so I suggest we take a look deeper, to the generated assembly code!
Now an interesting part, Rust compiler version <= 1.74.0 reserves 4096 + 16 = 4112 bytes of stack (yes, with optimization level 3, opt-level=3
), check this out https://godbolt.org/z/PsjETWceq:
example::fib1:
push r15
push r14
push rbx
sub rsp,0x1000 ; reserve 4096 bytes on stack
mov QWORD PTR [rsp],0x0
sub rsp,0x10 ; reserve another 16 bytes
mov r14d,esi
mov rbx,rdi
lea esi,[r14-0x1]
cmp esi,0x2
....
If I switch to Rust 1.75.0, it gets fixed, https://godbolt.org/z/63EE6j3h3, it reserves 32 bytes
example::fib1:
push r15
push r14
push rbx
sub rsp,0x20 ; reserve 32 bytes on stack
mov r14d,esi
mov rbx,rdi
lea esi,[r14-0x1]
cmp esi,0x2
I quickly checked the changes that landed in 1.75.0 and could not find anything. Any thoughts what was causing it? Is it a known bug?
Comments and suggestions are welcome! Thank you for your time.