`Result` optimization for two types with matching invalid patterns

type A = Result<() ()>;
type B = Result<() bool>;
type C = Result<bool, ()>;
type D = Result<bool, bool>;

A, B and C take up one byte because the compiler uses the invalid bit patterns (anything except 0x00 and 0x01) to represent the 1-bit information Result needs.

D however takes up two bytes, even though it has almost the same amount of available patterns. The compiler could use the following representations:

  • 0x00: Ok(false)
  • 0x01: Ok(true)
  • 0x10: Err(false)
  • 0x11: Err(true)

I know that rust doesn't use individual bits to optimize away that extra byte, but all patterns in the range 0x02..=0xff are unused/invalid for both the Ok and Err types.

I assume this is because the compiler only attempts this optimization if one of the types is ZST?

Is there another reason why this optimization isn't done?

Godbolt

For context: I've noticed this while looking through the Generic integers Pre-RFC:

Using a smaller number of bits for a value also has the benefit of niche enum optimisations. For example, u<7> represents a single ASCII character, and Option<u<7>> can be stored in a single byte. Additionally, Result<u<7>, E> also takes one byte if E is zero-sized.

This works because we know that values in the range 0b0_0000000..=0b0_1111111 are valid u<7>, but the values in the range 0b1_0000000..=0b1_1111111 are not and can represent the zero-sized variants None and Err(E).

In the case of D, it needs to be possible to get a shared reference to the bool inside the Result using only a shared reference to the Result. That means, for instance, that the representations for both Ok(false) and Err(false) need to contain a 0x00 byte.

8 Likes

See also this similar question posted last week on URLO. Why Doesn't Rust Use a Single Byte for Enum Tags When Possible? - #2 by SkiFire13 - The Rust Programming Language Forum

5 Likes

Also look up "move-only fields" for discussion of the feature that would let the user give up some things in order to give the compiler more freedom to do layout optimizations like this.

4 Likes

Consider this:

for possibility in [Ok(false), Ok(true), Err(false), Err(true)] {
    let value: &bool = match &possibility {
        Ok(value) => value,
        Err(value) => value,
    };

    let byte_representation: &u8 = unsafe { std::mem::transmute(value) };

    assert!(*byte_representation == 0 || *byte_representation == 1);
}

(Rust Playground)

Since byte_representation borrows from possibility, and since it only has a single byte, if Result<bool, bool> was optimized to also be a single byte they would have necessarily take up the same space. But according to the assertion - which must hold for booleans, according to Rust's rules - that byte can only have two options - 0 and 1. It is impossible to represent the four options of Result<bool, bool> with that single byte under these constraints.

1 Like

This is exactly what you've give up with move-only fields.

Now, we couldn't change Result to have that, but other types could, and by making it disallowed to take references, this problem goes away.

3 Likes

That reminds me quite a bit of the "pinned places" suggestion (almost the opposite), which takes away the ability to move the type (after some point in time), thus allowing self-references. If I understood it correctly a "move-only" or "not-referenceable" type/place/variable/value instead takes away the ability to make references to it (or its base fields), while keeping the ability to move the value.

Though that's probably not a good suggestion for this, as that would prevent layout optimizations (which as far as I can tell stay the same between pinned and not pinned).

And it adds a bunch of complexity for little benefit.

Anyways, thanks for all the responses (on a topic that probably should've been in the user forum).