unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
rust-lang/rust
senior8/10
compiler-internalsalgorithmic-optimizationerror-handling

Add intrinsics for integer minimum and maximum

Project: rust-lang/rust | Language: Rust | Concepts: compiler-internals, algorithmic-optimization, error-handling | Difficulty: senior
Link: https://github.com/rust-lang/rust/pull/161081

The problem

The current implementation of integer minimum and maximum operations in Rust, such as if a < b { a } else { b }, compiles down to inefficient MIR and LLVM IR. For example, u16::max emits many LLVM IR instructions including multiple basic blocks and stack allocations before optimizations, which is suboptimal and can hinder further compiler optimizations.

Why it's non-trivial

A naive fix might simply replace if expressions with calls to LLVM intrinsics, but:

  • LLVM and Cranelift backends must be updated to recognize and emit the appropriate intrinsic calls.
  • The intrinsic support must be guarded for types currently supported (e.g., up to 64 bits on Cranelift).
  • The Rust standard library must expose these intrinsics safely and stably.
  • The fallback implementation must be correct and compatible with Miri and CTFE.
  • The intrinsics must be integrated into Rust's type and intrinsic checking system.
  • Special cases like char require custom handling since their min/max are implemented differently.

The approach

1. Add intrinsic symbols for integer_min and integer_max

In rustc_span/src/symbol.rs:

+        integer_max,
+        integer_min,

This registers new intrinsic symbols used throughout the compiler.


2. Define the intrinsics in the Rust core library

In library/core/src/intrinsics/mod.rs:

#[rustc_const_unstable(feature = "const_cmp", issue = "143800")]
#[rustc_nounwind]
#[rustc_intrinsic]
#[miri::intrinsic_fallback_is_spec]
pub const fn integer_min<T: [const] bounds::IntegerPrimitive>(a: T, b: T) -> T {
    if a < b { a } else { b }
}

#[rustc_const_unstable(feature = "const_cmp", issue = "143800")]
#[rustc_nounwind]
#[rustc_intrinsic]
#[miri::intrinsic_fallback_is_spec]
pub const fn integer_max<T: [const] bounds::IntegerPrimitive>(a: T, b: T) -> T {
    if a < b { b } else { a }
}
  • These intrinsics are generic over integer primitive types.
  • They provide a fallback implementation for Miri and CTFE.
  • Marked as unstable for const evaluation improvements.
  • The fallback is simple and spec-compliant.

3. Define the integer primitive trait for type bounds

In library/core/src/intrinsics/bounds.rs:

pub const unsafe trait IntegerPrimitive: Copy + [const] Ord {}

macro_rules! impl_integer_primitive {
    ($($t:ty),*) => {$(
        #[rustc_const_unstable(feature = "core_intrinsics", issue = "none")]
        const unsafe impl IntegerPrimitive for $t {}
    )*};
}
impl_integer_primitive!(i8, i16, i32, i64, i128, isize);
impl_integer_primitive!(u8, u16, u32, u64, u128, usize);
  • This trait restricts the intrinsic to valid integer types.
  • bool and char are excluded intentionally.

4. Update Ord implementations to use the new intrinsics

In library/core/src/cmp.rs:

macro_rules! min_max_impl {
    (char) => {
        #[inline]
        fn min(self, other: Self) -> Self {
            let c = u32::min(self as u32, other as u32);
            unsafe { char::from_u32_unchecked(c) }
        }

        #[inline]
        fn max(self, other: Self) -> Self {
            let c = u32::max(self as u32, other as u32);
            unsafe { char::from_u32_unchecked(c) }
        }
    };
    ($t:ident) => {
        #[inline]
        fn min(self, other: Self) -> Self {
            crate::intrinsics::integer_min(self, other)
        }

        #[inline]
        fn max(self, other: Self) -> Self {
            crate::intrinsics::integer_max(self, other)
        }
    };
}
  • For integer types, min and max now call the new intrinsics.
  • For char, min/max are implemented via u32 min/max with safety comments.

5. Add intrinsic support in LLVM backend

In compiler/rustc_codegen_llvm/src/intrinsic.rs:

impl<'ll, 'tcx> IntrinsicCallBuilderMethods<'tcx> for Builder<'_, 'll, 'tcx> {
    ...
    sym::integer_min | sym::integer_max => {
        let lhs = args[0].immediate();
        let rhs = args[1].immediate();
        let llvm_name = match (name, signed) {
            (sym::integer_max, false) => "llvm.umax",
            (sym::integer_max, true) => "llvm.smax",
            (sym::integer_min, false) => "llvm.umin",
            (sym::integer_min, true) => "llvm.smin",
            _ => bug!(),
        };
        self.call_intrinsic(llvm_name, &[llty], &[lhs, rhs])
    }
}

In compiler/rustc_codegen_llvm/src/lib.rs:

+            sym::integer_max,
+            sym::integer_min,
  • Maps Rust intrinsics to LLVM intrinsics depending on signedness.
  • Registers the intrinsics in LLVM codegen backend.

6. Add intrinsic support in Cranelift backend

In compiler/rustc_codegen_cranelift/src/intrinsics/mod.rs:

sym::integer_max | sym::integer_min if ret.layout().size <= Size::from_bits(64) => {
    intrinsic_args!(fx, args => (lhs, rhs); intrinsic);

    assert_eq!(lhs.layout().ty, rhs.layout().ty);
    let signed = type_sign(lhs.layout().ty);
    let lhs = lhs.load_scalar(fx);
    let rhs = rhs.load_scalar(fx);
    let res = match (intrinsic, signed) {
        (sym::integer_max, false) => fx.bcx.ins().umax(lhs, rhs),
        (sym::integer_max, true) => fx.bcx.ins().smax(lhs, rhs),
        (sym::integer_min, false) => fx.bcx.ins().umin(lhs, rhs),
        (sym::integer_min, true) => fx.bcx.ins().smin(lhs, rhs),
        _ => unreachable!(),
    };

    let res = CValue::by_val(res, ret.layout());
    ret.write_cvalue(fx, res);
}
  • Uses Cranelift's umin, umax, smin, and smax instructions.
  • Currently limited to types up to 64 bits due to backend support.
  • Provides a fallback path for unsupported sizes.

7. Update intrinsic checks and type signatures

In compiler/rustc_hir_analysis/src/check/intrinsic.rs:

+        | sym::integer_max
+        | sym::integer_min

and

+        sym::integer_max | sym::integer_min => (1, 0, vec![param(0), param(0)], param(0)),
  • Adds the intrinsics to the unsafety and type checking logic.

8. Add tests and codegen checks

In tests/codegen-llvm/intrinsics/integer_min_max.rs:

#[unsafe(no_mangle)]
pub fn i16_min(a: i16, b: i16) -> i16 {
    // CHECK-LABEL: i16_min
    // CHECK: [[M:%.+]] = call i16 @llvm.smin.i16(i16 %a, i16 %b)
    // CHECK-NEXT: ret i16 [[M]]
    std::cmp::min(a, b)
}
  • Similar tests for other integer types and char.
  • Ensures LLVM emits the correct intrinsic calls.

9. Add unit tests for correctness

In library/coretests/tests/cmp.rs:

#[test]
fn test_max_min_signedness() {
    use std::cmp::{max, min};
    assert_eq!(max::<u8>(0, 255), 255);
    assert_eq!(max::<u8>(255, 0), 255);
    assert_eq!(min::<u8>(0, 255), 0);
    assert_eq!(min::<u8>(255, 0), 0);
    assert_eq!(max::<i8>(0, -1), 0);
    assert_eq!(max::<i8>(-1, 0), 0);
    assert_eq!(min::<i8>(0, -1), -1);
    assert_eq!(min::<i8>(-1, 0), -1);
}
  • Verifies signedness correctness of min and max.

From the review thread

clarfonthey:

I'm happy with this and would be fine merging once you make whatever fallback changes you were mentioning. I'm not on the compiler team, but just from the perspective of optimising the giant pile of operations we have in libstd, adding more intrinsics for "obvious" primitives like this is fine, even if they're technically redundant.
For example, #161069 which also fell under my review recommended adding some potential additional reasoning for checked_next_power_of_two and I was already a bit worried that the complexity of the operations involved would not optimise correctly, which I haven't yet verified. If the codegen backends support it, I think we should go ahead with these, especially if there's some extra empirical justification as there seems to be here.

No other notable review discussion.

Takeaways

  • When compiler backends provide dedicated instructions or intrinsics for common operations, exposing those intrinsics in the language can reduce codegen complexity and improve optimization opportunities.
  • Providing a fallback implementation for intrinsics is important to maintain compatibility with interpreters (like Miri), CTFE, and alternative codegen backends.
  • Intrinsics should be carefully integrated into the compiler's type and unsafety checking systems to ensure soundness and correct usage.
  • Special cases (like char) may require custom implementations even when adding intrinsics for primitives.