Why can't conversion to an enum be inlined?

#1

This is entirely out of curiosity, but is there a reason why the in the following code using_enum produces more instructions than using_u8? I would have thought that the output would be identical.

Godbolt link

pub fn using_u8(n: u8) -> u8 {
    let row_type = n % 3;

    match row_type {
        0 => a(n),
        1 => b(n),
        _ => c(n),
    }
}

enum RowType {
    Left, Middle, Right
}

fn get_row_type(n: u8) -> RowType {
    let row_type = n % 3;

    match row_type {
        0 => RowType::Left,
        1 => RowType::Middle,
        _ => RowType::Right,
    }
}

pub fn using_enum(n: u8) -> u8 {
    match get_row_type(n) {
        RowType::Left => a(n),
        RowType::Middle => b(n),
        RowType::Right => c(n),
    }
}

#[inline(never)]
fn a(n: u8) -> u8 { n ^ 0b1010101 }
#[inline(never)]
fn b(n: u8) -> u8 { n ^ 0b1101 }
#[inline(never)]
fn c(n: u8) -> u8 { n ^ 0b10101 }
0 Likes

#2

Both things aren’t really comparable: the enum example performs 2 matches, whereas your u8 example does not.

I’ve made the following example, which is better reflects what the enum does (I’ve chosen to use #[repr(u8)] since I am using u8 constants):

const LEFT   : u8 = 0;
const MIDDLE : u8 = 1;
const RIGHT  : u8 = 2;

pub fn get_row_type_u8(n: u8) -> u8 {
    let row_type = n % 3;

    match row_type {
        0 => LEFT,
        1 => MIDDLE,
        _ => RIGHT,
    }

}

#[repr(u8)]
enum RowType {
    Left, Middle, Right
}

fn get_row_type_enum(n: u8) -> RowType {
    let row_type = n % 3;

    match row_type {
        0 => RowType::Left,
        1 => RowType::Middle,
        _ => RowType::Right,
    }
}

#[no_mangle]
pub
fn using_enum(n: u8) -> u8 {
    match get_row_type_enum(n) {
        RowType::Left => a(n),
        RowType::Middle => b(n),
        RowType::Right => c(n),
    }
}

#[no_mangle]
pub
fn using_u8(n: u8) -> u8 {
    match get_row_type_u8(n) {
        LEFT => a(n),
        MIDDLE => b(n),
        RIGHT => c(n),
        _ => unsafe { std::hint::unreachable_unchecked() },
    }
}

#[inline(never)]
fn a(n: u8) -> u8 { n ^ 0b1010101 }
#[inline(never)]
fn b(n: u8) -> u8 { n ^ 0b1101 }
#[inline(never)]
fn c(n: u8) -> u8 { n ^ 0b10101 }


/// An #[inline(always)] std::hint::unreachable_unchecked()
mod std {
    pub mod hint {
        #[inline(always)]
        /// UB UB UB
        pub unsafe fn unreachable_unchecked () -> !
        {
            struct VoidStruct; enum VoidType {}
            match ::std::mem::transmute::<_, VoidType>(VoidStruct) {}
        }
    }
}

This then gives (Godbolt can’t handle explicit unreachable hints)

They are outstandingly similar.

Which is confirmed when building in --release mode:

$ nm ./target/release/libtemp.so | grep using
0000000000000960 T using_enum
0000000000000960 T using_u8

Thus, the problem does not seem to come from using enums but from the 2 consecutive matches.

2 Likes

#3

It looks like a missed optimization somewhere. When I change the match statement in get_row_type to the below, using_u8 and using_enum produce the exact same assembly such that it is unified into one function.

    match row_type {
        0 => RowType::Left,
        1 => RowType::Middle,
        2 => RowType::Right,
        _ => RowType::Right,
    }
4 Likes