Which one has better Assembly output between Pattern Matching Enum and Fixed Size Array Enum Lookup?

⚓ Rust    📅 2026-01-18    👤 surdeus    👁️ 2      

surdeus

I'm trying to understand this Assembly output between matching enum and fixed size array enum lookup

Code 1:

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum LogLevel {
    Trace = 0,
    Debug = 1,
    Info = 2,
    Warn = 3,
    Error = 4,
}

impl LogLevel {
    #[inline(always)]
    const fn as_bytes(self) -> &'static [u8] {
        match self {
            LogLevel::Trace => b"TRACE",
            LogLevel::Debug => b"\x1b[34mDEBUG\x1b[0m",
            LogLevel::Info => b"\x1b[32mINFO\x1b[0m",
            LogLevel::Warn => b"\x1b[33mWARN\x1b[0m",
            LogLevel::Error => b"\x1b[31mERROR\x1b[0m",
        }
    }
}

#[unsafe(no_mangle)]
fn task(a: LogLevel) -> &'static [u8] {
    a.as_bytes()
}
task:
        lea     rax, [rip + .Lswitch.table.task]
        movzx   ecx, dil
        mov     rdx, qword ptr [rax + 8*rcx]
        lea     rsi, [rip + .Lswitch.table.task.1.rel]
        movsxd  rax, dword ptr [rsi + 4*rcx]
        add     rax, rsi
        ret

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.0:
        .ascii  "TRACE"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.1:
        .ascii  "\033[34mDEBUG\033[0m"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.2:
        .ascii  "\033[32mINFO\033[0m"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.3:
        .ascii  "\033[33mWARN\033[0m"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.4:
        .ascii  "\033[31mERROR\033[0m"

.Lswitch.table.task:
        .quad   5
        .quad   14
        .quad   13
        .quad   13
        .quad   14

.Lswitch.table.task.1.rel:
        .long   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.0-.Lswitch.table.task.1.rel
        .long   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.1-.Lswitch.table.task.1.rel
        .long   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.2-.Lswitch.table.task.1.rel
        .long   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.3-.Lswitch.table.task.1.rel
        .long   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.4-.Lswitch.table.task.1.rel

Code 2:

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum LogLevel {
    Trace = 0,
    Debug = 1,
    Info = 2,
    Warn = 3,
    Error = 4,
}

impl LogLevel {
    #[inline(always)]
    const fn as_bytes(self) -> &'static [u8] {
        const LOOKUP: &[&[u8]] = &[
            b"TRACE",
            b"\x1b[34mDEBUG\x1b[0m",
            b"\x1b[32mINFO\x1b[0m",
            b"\x1b[33mWARN\x1b[0m",
            b"\x1b[31mERROR\x1b[0m",
        ];
        LOOKUP[self as usize]
    }
}

#[unsafe(no_mangle)]
fn task(a: LogLevel) -> &'static[u8] {
    a.as_bytes()
}

task:
        movzx   ecx, dil
        shl     ecx, 4
        lea     rdx, [rip + .Lanon.478f3a4c51824ad23cb50c1c60670c0f.5]
        mov     rax, qword ptr [rcx + rdx]
        mov     rdx, qword ptr [rcx + rdx + 8]
        ret

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.0:
        .ascii  "TRACE"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.1:
        .ascii  "\033[34mDEBUG\033[0m"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.2:
        .ascii  "\033[32mINFO\033[0m"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.3:
        .ascii  "\033[33mWARN\033[0m"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.4:
        .ascii  "\033[31mERROR\033[0m"

.Lanon.478f3a4c51824ad23cb50c1c60670c0f.5:
        .quad   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.0
        .asciz  "\005\000\000\000\000\000\000"
        .quad   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.1
        .asciz  "\016\000\000\000\000\000\000"
        .quad   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.2
        .asciz  "\r\000\000\000\000\000\000"
        .quad   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.3
        .asciz  "\r\000\000\000\000\000\000"
        .quad   .Lanon.478f3a4c51824ad23cb50c1c60670c0f.4
        .asciz  "\016\000\000\000\000\000\000"

If the LogLevel is known at compile time they produce the same Assembly. But if LogLevel is known at runtime they produce different Assembly as shown above. My interpretation is Code 2 is better because 1 linear table, fewer instructions, records are stored in contigously that move will read memory address that close to each other continously (cache localicy for better cache hit). Where Code 2 has 2 different switch tables that if the table is big will cause cache miss, and has more instructions

What is your interpretation version?

1 post - 1 participant

Read full topic

🏷️ Rust_feed