rust-lang / rust-lang/rust

`MatchBranchSimplification` destroys power-of-two knowledge for `#[repr(u8)]` enums whose discriminants equal to match arms

Open
#162,513 3 comments 0 reactions 1 assignee View on GitHub

@dianqk is already working on this.

Since Sep 9, 2026.

A-codegen A-mir-opt C-optimization needs-triage
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Reproduction:

#![crate_type = "lib"]

#[derive(Clone, Copy)]
#[repr(u8)]
pub enum Vsew { E8 = 8, E16 = 16, E32 = 32, E64 = 64 }

impl Vsew {
    #[inline(always)]
    pub const fn bits_width(self) -> u8 {
        match self {
            Self::E8 => 8,
            Self::E16 => 16,
            Self::E32 => 32,
            Self::E64 => 64,
        }
    }
}

#[unsafe(no_mangle)]
pub fn vlmax(sew: Vsew) -> u32 {
    512 / u32::from(sew.bits_width())
}

https://rust.godbolt.org/z/f6Y4Yq4b6

In this example vlmax, unfortunately, retains faithful division operation (rustc 1.100.0-nightly (cea272fa3 2026-09-07)):

vlmax:
        movzx   ecx, dil
        mov     ax, 512
        xor     edx, edx
        div     cx
        movzx   eax, ax
        ret

Compiling with -Zmir-enable-passes=-MatchBranchSimplification generates much better code here:

vlmax:
        rep     bsf ecx, edi
        add     cl, -3
        mov     eax, 64
        shr     eax, cl
        ret
More closely related examples
#![crate_type = "lib"]

#[derive(Clone, Copy)]
#[repr(u8)]
pub enum Vsew {
    E8 = 8,
    E16 = 16,
    E32 = 32,
    E64 = 64,
}

impl Vsew {
    #[inline(always)]
    pub const fn bits_width(self) -> u8 {
        match self {
            Self::E8 => 8,
            Self::E16 => 16,
            Self::E32 => 32,
            Self::E64 => 64,
        }
    }
}

#[repr(u8)]
pub enum Lmul {
    M1 = 1,
    M2 = 2,
    M4 = 4,
    M8 = 8,
}
impl Lmul {
    #[inline(always)]
    pub const fn num(self) -> u8 {
        match self {
            Self::M1 => 1,
            Self::M2 => 2,
            Self::M4 => 4,
            Self::M8 => 8,
        }
    }
}

#[unsafe(no_mangle)]
pub fn aligned(x: u8, g: Lmul) -> bool {
    // div
    x.is_multiple_of(g.num())
}

#[unsafe(no_mangle)]
pub fn elem_div(i: u32, sew: Vsew) -> (u32, u32) {
    // 2x div
    let epr = 64 / u32::from(sew.bits_width() / 8);
    (i / epr, i % epr)
}

// x / min(a, b) and x / (a * b) with all operands powers of two
#[unsafe(no_mangle)]
pub fn index_register_count(index: Vsew, sew: Vsew, ln: Lmul, ld: Lmul) -> (u16, u16) {
    let num = u16::from(index.bits_width()) * u16::from(ln.num());
    let den = u16::from(sew.bits_width()) * u16::from(ld.num());
    let g = if num < den { num } else { den };
    (num / g, den / g) // 2x div
}

https://rust.godbolt.org/z/WY7b8dsGn

The workaround is to write explicit assertions like this:

let w = sew.bits_width();
unsafe { core::hint::assert_unchecked(w.is_power_of_two()) };
512 / u32::from(w)

https://rust.godbolt.org/z/hx9dGMPfz

Which (another surprise) generates better code still (but only as long as MatchBranchSimplification is enabled, otherwise extra instruction appears):

vlmax:
        rep     bsf ecx, edi
        mov     eax, 512
        shr     eax, cl
        ret
LLM-generated root cause analysis and potential solutions

Summary

For a #[repr(uN)] enum whose discriminants are exactly the values a match maps them to
(a very common "unit -> numeric width" pattern), the MatchBranchSimplification MIR pass replaces
the SwitchInt with a plain cast of the discriminant. The only fact that survives into LLVM IR is
the convex hull range(i8 8, 65), so LLVM can no longer prove the value is a power of two and
emits a hardware div/idiv.

Without the pass (-Zmir-opt-level=0 or -Zmir-enable-passes=-MatchBranchSimplification) LLVM sees
the switch, folds it into a shift map, and emits bsf+shr. So the MIR pass is a pessimisation
here: it removes information that LLVM was already able to exploit.

Environment

rustc 1.100.0-nightly (e7769602a 2026-08-24)
host: x86_64-unknown-linux-gnu
LLVM version: 23.1.0

Also reproduces on stable-shaped code; flags: rustc -O --crate-type lib.

Reproduction

#![crate_type = "lib"]

#[derive(Clone, Copy)]
#[repr(u8)]
pub enum Vsew { E8 = 8, E16 = 16, E32 = 32, E64 = 64 }

impl Vsew {
    #[inline(always)]
    pub const fn bits_width(self) -> u8 {
        match self {
            Self::E8 => 8,
            Self::E16 => 16,
            Self::E32 => 32,
            Self::E64 => 64,
        }
    }
}

#[no_mangle]
pub fn vlmax(sew: Vsew) -> u32 {
    512 / u32::from(sew.bits_width())
}
Actual (rustc -O)
vlmax:
        movzbl  %dil, %ecx
        movw    $512, %ax
        xorl    %edx, %edx
        divw    %cx            ; <-- hardware division
        movzwl  %ax, %eax
        retq
Expected (rustc -O -Zmir-opt-level=0, or -Zmir-enable-passes=-MatchBranchSimplification)
vlmax:
        rep     bsfl %edi, %ecx
        addb    $-3, %cl
        movl    $64, %eax
        shrl    %cl, %eax
        retq

Both spellings produce the same MIR after the pass, so the same div results:

// identical output
pub fn e(s: Vsew) -> u32 { let w: u32 = match s { Vsew::E8 => 8, Vsew::E16 => 16,
                                                  Vsew::E32 => 32, Vsew::E64 => 64 }; 512 / w }

Where the information is lost

rustc -O -C no-prepopulate-passes --emit llvm-ir (default MIR opts) — the switch is already gone
before LLVM runs, and only a hull range remains:

define noundef i32 @vlmax(i8 noundef range(i8 8, 65) %sew) unnamed_addr {
start:
  %_2 = zext i8 %sew to i32
  %_4 = icmp eq i32 %_2, 0
  br i1 %_4, label %panic, label %bb1
bb1:
  %_0 = udiv i32 512, %_2
  ret i32 %_0
...

rustc -O -Zmir-opt-level=0 -C no-prepopulate-passes --emit llvm-ir — the switch is intact, and
LLVM's SimplifyCFG/InstCombine turn it into a shift:

define noundef i32 @vlmax(i8 noundef range(i8 8, 65) %sew) unnamed_addr {
start:
  switch i8 %sew, label %bb1 [
    i8 8,  label %bb5
    i8 16, label %bb4
    i8 32, label %bb3
    i8 64, label %bb2
  ]
  ...
  %_0 = udiv i32 512, %_4

Bisecting the MIR passes pins it exactly:

-Zmir-enable-passes=-MatchBranchSimplification   -> optimized (bsf/shr)
-Zmir-enable-passes=-GVN                         -> still div
-Zmir-enable-passes=-JumpThreading                -> still div
-Zmir-enable-passes=-EarlyOtherwiseBranch         -> still div
-Zmir-enable-passes=-SingleUseConsts              -> still div
-Zmir-enable-passes=-SimplifyComparisonIntegral   -> still div

The problem is not specific to a match in the same function: the same happens when the enum is
loaded from memory, because bits_width() is inlined and simplified first, leaving

%_4 = load i8, ptr %s, align 1, !range !6, !noundef !7
...
!6 = !{i8 8, i8 65}          ; hull, not {8,16,32,64}

Other affected shapes (same root cause)

All of these emit div/idiv today and all of them become shift/mask with
-Zmir-enable-passes=-MatchBranchSimplification:

#[repr(u8)] pub enum Lmul { M1 = 1, M2 = 2, M4 = 4, M8 = 8 }
impl Lmul { #[inline(always)] pub const fn num(self) -> u8 {
    match self { Self::M1 => 1, Self::M2 => 2, Self::M4 => 4, Self::M8 => 8 } } }

#[no_mangle] pub fn aligned(x: u8, g: Lmul) -> bool { x.is_multiple_of(g.num()) }        // divb

#[no_mangle] pub fn elem_div(i: u32, sew: Vsew) -> (u32, u32) {                          // divb + divl
    let epr = 64 / u32::from(sew.bits_width() / 8);
    (i / epr, i % epr)
}

// x / min(a, b) and x / (a * b) with all operands powers of two
#[no_mangle] pub fn index_register_count(index: Vsew, sew: Vsew, ln: Lmul, ld: Lmul) -> (u16, u16) {
    let num = u16::from(index.bits_width()) * u16::from(ln.num());
    let den = u16::from(sew.bits_width())   * u16::from(ld.num());
    let g = if num < den { num } else { den };
    (num / g, den / g)                                                                    // 2x divw
}

LLVM already knows pow2 * pow2 and min(pow2, pow2) are powers of two, so once the fact reaches
it, the min/mul variants fold too — no extra LLVM support is needed for those.

Workaround

let w = sew.bits_width();
unsafe { core::hint::assert_unchecked(w.is_power_of_two()) };
512 / u32::from(w)
vlmax_workaround:
        rep     bsfl %edi, %ecx
        movl    $512, %eax
        shrl    %cl, %eax
        retq

Rewriting bits_width as 8u8 << k with k in 0..=3, or using sequential discriminants
E8 = 0, E16 = 1, ..., also avoids the problem (in the latter case the pass produces an
exponential/linear form that LLVM can still reason about).

Possible fixes

  1. Have MatchBranchSimplification avoid replacing a SwitchInt with a bare cast when the
    discriminant set is non-contiguous and the result is not otherwise simplified — the switch is a
    strictly more informative encoding, and LLVM handles it well.
  2. Or preserve the fact: LLVM's !range metadata supports multiple disjoint intervals, so an
    enum load could carry !{i8 8, i8 9, i8 16, i8 17, i8 32, i8 33, i8 64, i8 65} instead of
    !{i8 8, i8 65}. (The range(..) parameter attribute only allows a single interval, so
    arguments would need llvm.assume or a different mechanism — and LLVM would also need to learn
    to use such information; see the companion LLVM report.)

Related LLVM work: llvm/llvm-project#115767 (fixed) made LLVM strength-reduce udiv/urem once the
power-of-two property is known, which is why the assert_unchecked workaround works.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.