Arc plus sub-enum blow up parent enum size

I have en enum Error as below. rustc complains about this via the lint variant-size-differences.

However, I don't understand why putting the variants with the Arc and ParseError together blows up the enum size so much.

use std::sync::Arc;
use std::mem::size_of;

#[derive(Clone, Debug)]
pub enum Error {
    IoError(Arc<std::io::Error>),
    InvalidVersion(ParseError),
    VersionNotFound,
}

#[derive(Clone, Debug)]
pub enum Error2 {
    IoError(Arc<std::io::Error>),
}

#[derive(Clone, Debug)]
pub enum Error3 {
    InvalidVersion(ParseError),
    VersionNotFound,
}

#[derive(Clone, Debug)]
pub enum Error4 {
    IoError(Arc<std::io::Error>),
    InvalidVersion(ParseError),
}

#[derive(Clone, Debug)]
pub enum Error5 {
    IoError(Arc<std::io::Error>),
    VersionNotFound,
}

#[derive(Clone, Debug)]
pub enum Error6 {
    VersionNotFound,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum ParseError {
    NotExactlyThreeFields,
    NonNumericFields,
}

fn main() {
    println!("Error: {}", size_of::<Error>());
    println!("Error2: {}", size_of::<Error2>());
    println!("Error3: {}", size_of::<Error3>());
    println!("Error4: {}", size_of::<Error4>());
    println!("Error5: {}", size_of::<Error5>());
    println!("Error6: {}", size_of::<Error6>());
    println!("Arc<()>: {}", size_of::<Arc<()>>());
    println!("Arc<std::io::Error>: {}", size_of::<Arc<std::io::Error>>());
    println!("ParseError: {}", size_of::<ParseError>());
}

Output:

Error: 16
Error2: 8
Error3: 1
Error4: 16
Error5: 8
Error6: 0
Arc<()>: 8
Arc<std::io::Error>: 8
ParseError: 1

What am I missing here?

Arc has an alignment requirement of 8 bytes which means that as soon as the enum discriminant can’t fit in Arc’s niche¹ the whole type has to grow by another 8 bytes to 16 in order to maintain the field alignment. The “good” news is that most of the extra size is padding, which means that you can add lots more variants to this enum without making it any bigger again.

¹ The internal pointer can never be null, so there’s room for the compiler to stuff a single unit enum variant in the same space. More than one extra, or a single extra variant with data, is too much to hide there.

3 Likes