How much overhead is there with Options and Results?

In short, I would like to know how efficient it is to return an Option versus just returning a value. For example:

struct Example_Struct {
    info: u32
}

// function returning an object
fn get_struct() -> Example_Struct {
    Example_Struct {info: 1234}
}

// function returning an option
fn get_struct_option() -> Option<Example_Struct> {
    Some(Example_Struct {info: 1234})
}

What would the difference in overhead be for calling get_struct(), versus calling get_struct_option().unwrap()?

Fairly little. In terms of space, ran Option<Example_Struct> (on 64 bit x86) is 8 bytes as opposed to Example_Struct's 4 bytes, which is double, but it's not a particularly big struct anyway, and it can be as little as an extra byte, or no extra bytes if returning an enum, since the optimizer can just fill in the niche with None. Unwrapping technically requires more processing, since it has a chance of panicking, but it's only a couple instructions.

Essentially, the overhead you get from using Option or Result is essentially the same as you'd get by having any other check that a value is valid (returning an int condition or something like that.) If you don't need the possibility of None, don't use Option, and if you do, there isn't often a way one could do it better than using Option/Result.

6 Likes

It's also worth noting that this is not an Option-specific optimization!

If any enum has:

  • One variant with no data
  • One variant that contains a type which cannot have all zeros as its bit pattern

...then the compiler will use an all-zero bit pattern to represent the first variant.

1 Like

A niche doesn't have to contain the all-zeros bit pattern; for example, Option<bool> will use 0 for Some(false), 1 for Some(true), and 2 for None.

If you wrap it again, Option<Option<bool>> still fits in u8 and uses 3 for None. I believe you can keep doing this all the way up to

Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<Option<bool>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Which would, of course, use 255 for None.

2 Likes

Oh, so it's not that it must have a non-zero bit pattern, rather, it must have some kind of bit pattern that isn't a valid value? TIL, thank you :slight_smile:

1 Like

Yes, niches can be other bit patterns besides all-zeros.

For the closest thing we have to an "official" definition of "niche", see the UCG glossary: https://github.com/rust-lang/unsafe-code-guidelines/blob/636d140ce9c74ffc4d1fc082bef0771f238f64c9/reference/src/glossary.md#niche

Some may also be interested in https://github.com/rust-lang/rust/pull/45225, which afaik is the biggest extension to niche-using layout optimizations since Rust 1.0, and it's still most of the answer to "how much of this does the compiler actually do today?"

2 Likes

Your belief appears to be borne out in practice, but you need to increase the recursion limit to get it to compile: see here.

The output is:
[src/main.rs:5] std::mem::size_of_val(&a) = 1

3 Likes