Skip to content

Unnecesary discriminant checks in PartialEq/PartialOrd style code on enums #119014

Closed

Description

@Kmeakin

I tried this code:
compiler explorer

pub enum Enum {
    A(u32),
    B(u32),
    C(u32),
}

#[no_mangle]
pub fn f1(lhs: &Enum, rhs: &Enum) -> bool {
    match (lhs, rhs) {
        (Enum::A(lhs), Enum::A(rhs)) => lhs == rhs,
        (Enum::B(lhs), Enum::B(rhs)) => lhs == rhs,
        (Enum::C(lhs), Enum::C(rhs)) => lhs == rhs,
        _ => false,
    }
}

#[no_mangle]
pub fn f2(lhs: &Enum, rhs: &Enum) -> bool {
    if std::mem::discriminant(lhs) != std::mem::discriminant(rhs) {
        return false;
    }

    match (lhs, rhs) {
        (Enum::A(lhs), Enum::A(rhs)) => lhs == rhs,
        (Enum::B(lhs), Enum::B(rhs)) => lhs == rhs,
        (Enum::C(lhs), Enum::C(rhs)) => lhs == rhs,
        _ => false,
    }
}

#[no_mangle]
pub fn f3(lhs: &Enum, rhs: &Enum) -> bool {
    if std::mem::discriminant(lhs) != std::mem::discriminant(rhs) {
        return false;
    }

    match (lhs, rhs) {
        (Enum::A(lhs), Enum::A(rhs)) => lhs == rhs,
        (Enum::B(lhs), Enum::B(rhs)) => lhs == rhs,
        (Enum::C(lhs), Enum::C(rhs)) => lhs == rhs,
        _ => unsafe { std::hint::unreachable_unchecked() },
    }
}

Code like this is often used for implementations of PartialEq or PartialOrd on enums, so it is important that it be optimized well.

I expected to see this happen:
The functions f1 and f2 should produce equivalent machine code to f3.

Instead, this happened:
f1 and f2 do not produce equivalent machine code.
f1 is missing the early return for when the discriminants do not match.
f2 unnecessarily examines the discriminant of lhs twice!
LLVM should be able to transform f1 into f2, and f2 into f3

Meta

rustc --version --verbose:

rustc 1.76.0-nightly (a96d57bdb 2023-12-15)
binary: rustc
commit-hash: a96d57bdb6d2bb6d233d7d5aaefc2995ab99be01
commit-date: 2023-12-15
host: x86_64-unknown-linux-gnu
release: 1.76.0-nightly
LLVM version: 17.0.6
Backtrace

<backtrace>

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Dec 16, 2023
  2. Kmeakin commented on Dec 16, 2023

    @Kmeakin
    ContributorAuthor

    @rustbot label I-slow

  3. added
    I-slowIssue: Problems and improvements with respect to performance of generated code.
    on Dec 16, 2023
  4. Kmeakin commented on Dec 16, 2023

    @Kmeakin
    ContributorAuthor

    Interestingly, when Enum is a c-like enum, f1 produces the same code as f3, but f2 doesnt: https://godbolt.org/z/81bGdq6xs

  5. Kmeakin commented on Dec 16, 2023

    @Kmeakin
    ContributorAuthor

    Adding a zero-sized field (like ()) even prevents f1 being optimized into f3 (https://godbolt.org/z/3dqzTfofa), but an empty field list doesn't (https://godbolt.org/z/bTK9qa9z1)

  6. clubby789 commented on Dec 16, 2023

    @clubby789
    Contributor

    Looks similar to #113506, which was fixed upstream.
    Ah, a little different
    Throwing f1 into upstream opt improves codegen, but still not as good as the optimal case in f3

    The PartialEq derive uses essentially the same code as f3 because of this issue, so it would be nice to have it work here

  7. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    and removed
    C-bugCategory: This is a bug.
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Dec 16, 2023
  8. Kmeakin commented on Dec 17, 2023

    @Kmeakin
    ContributorAuthor

    Looks similar to #113506, which was fixed upstream

    The improvement I'm looking for here is removing unnecessary repeated inspections of enum discriminants, not merging duplicate RHS per se. This improvement would still be useful even if each enum variant has different data in it:

    compiler explorer

    pub enum Enum {
        A(u32),
        B(u64),
        C(f32),
        D(f64),
    }
  9. clubby789 commented on Jan 10, 2024

    @clubby789
    Contributor

    Here's some more similar strange behaviour:

    #[no_mangle]
    pub fn f1(lhs: &Enum, rhs: &Enum) -> bool {
        match (lhs, rhs) {
            (Enum::A(lhs), Enum::A(rhs)) => true,
            (Enum::B(lhs), Enum::B(rhs)) => true,
            (Enum::C(lhs), Enum::C(rhs)) => true,
            _ => false,
        }
    }

    demonstrates the repeated inspections, but if we remove the unused lhs/rhs bindings (or have bindings in only one branch), we get optimal codegen:
    https://godbolt.org/z/fPMx5bYz6

    Seems like MIR optimisations manage to improve it to a point where LLVM can optimise it better

  10. dianqk commented on Feb 7, 2024

    @dianqk
    Member

    Update: In fact, we just need to re-enable the early otherwise branch optimization.


    Hmm, this requires multiple optimizations to work together.

    As we merge the two i32, we'll end up with better instructions. See https://llvm.godbolt.org/z/EYG46EaxY.

    mov     rax, qword ptr [rsi]
    cmp     rax, qword ptr [rdi]
    sete    al
    ret

    It may be that part of the optimization fits in MIR, I'll trade off.

    @rustbot claim

  11. added a commit that references this issue on Feb 29, 2024
    7a8b62c
  12. added a commit that references this issue on Mar 3, 2024
  13. added a commit that references this issue on Mar 3, 2024
    a4a739a
  14. added a commit that references this issue on Mar 27, 2024
    1c207eb
  15. added a commit that references this issue on Apr 7, 2024
    7427e2c
  16. added a commit that references this issue on Apr 8, 2024
  17. added a commit that references this issue on Apr 9, 2024
    59c808f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.I-slowIssue: Problems and improvements with respect to performance of generated code.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions