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
charrequire 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.
boolandcharare 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,
minandmaxnow call the new intrinsics. - For
char, min/max are implemented viau32min/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, andsmaxinstructions. - 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 forchecked_next_power_of_twoand 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.