Skip to content

Commit be4b6a9

Browse files
committed
Auto merge of #161081 - scottmcm:min-max-intrinsics, r=clarfonthey
Add intrinsics for integer minimum and maximum I got inspired to do this when looking at `SliceOrd::compare` where I was reminded that `if a < b { a } else { b }` isn't great in MIR since it takes 4 BBs. Looking at the codegen side, it turns out we currently emit [42 lines of LLVM-IR including 4 `alloca`s](https://rust.godbolt.org/z/ha4h4nT3r) for `u16::max` (pre-optimization), which is also unnecessarily bad†. But both LLVM and Cranelift have dedicated things for min & max: - https://llvm.org/docs/LangRef.html#llvm-umax-intrinsic - https://docs.rs/cranelift-codegen/latest/cranelift_codegen/ir/trait.InstBuilder.html#method.smin so let's just use those directly! This actually wouldn't have been worth doing originally, but a couple of things have happened to change that: - Back in 1.0 there was only `cmp::min` & `cmp::max`, so there was no place to actually do this at all, but in 2017 they were added to `Ord` as overridable things #25663 (comment) - LLVM originally used icmp+select for these, not a dedicated construct, but then added one and as of 2022 the intrinsic is fully usable https://www.npopov.com/2022/12/20/This-year-in-LLVM-2022.html#integer-minmax-intrinsics - Before we had intrinsic fallback this would have been more annoying to support everywhere -- GCC, [128-bit numbers on cg_clif](bytecodealliance/wasmtime#13790), CTFE, anything out-of-tree -- but now that we can write the obvious fallback we don't need to worry about that. - The intrinsic would have helped less when it forced extra BBs and `alloca`s in codegen anyway, but [now](rust-lang/compiler-team#970) we can keep the result in SSA without needing to make it a primitive. † Admittedly we could clean up the gratuitous badness there without needing an intrinsic, but I like doing the intrinsic anyway because that's the only way to avoid it always being stuck in the non-SSA path from the multi-BB assignments. Even if we made it inlineable, GVN and such will still just give up on seeing the `x = if a < b { a } else { b }` because it's multiple assignments to the same Local, which is non-ideal for something primitive-like.
2 parents 5321a4f + 7519f42 commit be4b6a9

10 files changed

Lines changed: 184 additions & 1 deletion

File tree

compiler/rustc_codegen_cranelift/src/intrinsics/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,26 @@ fn codegen_regular_intrinsic_call<'tcx>(
630630
let res = crate::num::codegen_int_binop(fx, BinOp::Div, x, y);
631631
ret.write_cvalue(fx, res);
632632
}
633+
// FIXME: remove the guard here once `umin.i128` and friends are supported
634+
// cc https://github.com/bytecodealliance/wasmtime/issues/13790
635+
sym::integer_max | sym::integer_min if ret.layout().size <= Size::from_bits(64) => {
636+
intrinsic_args!(fx, args => (lhs, rhs); intrinsic);
637+
638+
assert_eq!(lhs.layout().ty, rhs.layout().ty);
639+
let signed = type_sign(lhs.layout().ty);
640+
let lhs = lhs.load_scalar(fx);
641+
let rhs = rhs.load_scalar(fx);
642+
let res = match (intrinsic, signed) {
643+
(sym::integer_max, false) => fx.bcx.ins().umax(lhs, rhs),
644+
(sym::integer_max, true) => fx.bcx.ins().smax(lhs, rhs),
645+
(sym::integer_min, false) => fx.bcx.ins().umin(lhs, rhs),
646+
(sym::integer_min, true) => fx.bcx.ins().smin(lhs, rhs),
647+
_ => unreachable!(),
648+
};
649+
650+
let res = CValue::by_val(res, ret.layout());
651+
ret.write_cvalue(fx, res);
652+
}
633653
sym::saturating_add | sym::saturating_sub => {
634654
intrinsic_args!(fx, args => (lhs, rhs); intrinsic);
635655

compiler/rustc_codegen_llvm/src/intrinsic.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,8 @@ impl<'ll, 'tcx> IntrinsicCallBuilderMethods<'tcx> for Builder<'_, 'll, 'tcx> {
476476
| sym::ctpop
477477
| sym::bswap
478478
| sym::bitreverse
479+
| sym::integer_max
480+
| sym::integer_min
479481
| sym::saturating_add
480482
| sym::saturating_sub
481483
| sym::unchecked_funnel_shl
@@ -520,6 +522,18 @@ impl<'ll, 'tcx> IntrinsicCallBuilderMethods<'tcx> for Builder<'_, 'll, 'tcx> {
520522
sym::bitreverse => {
521523
self.call_intrinsic("llvm.bitreverse", &[llty], &[args[0].immediate()])
522524
}
525+
sym::integer_min | sym::integer_max => {
526+
let lhs = args[0].immediate();
527+
let rhs = args[1].immediate();
528+
let llvm_name = match (name, signed) {
529+
(sym::integer_max, false) => "llvm.umax",
530+
(sym::integer_max, true) => "llvm.smax",
531+
(sym::integer_min, false) => "llvm.umin",
532+
(sym::integer_min, true) => "llvm.smin",
533+
_ => bug!(),
534+
};
535+
self.call_intrinsic(llvm_name, &[llty], &[lhs, rhs])
536+
}
523537
sym::unchecked_funnel_shl | sym::unchecked_funnel_shr => {
524538
let is_left = name == sym::unchecked_funnel_shl;
525539
let lhs = args[0].immediate();

compiler/rustc_codegen_llvm/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ impl CodegenBackend for LlvmCodegenBackend {
333333
sym::unchecked_funnel_shl,
334334
sym::unchecked_funnel_shr,
335335
sym::carrying_mul_add,
336+
sym::integer_max,
337+
sym::integer_min,
336338

337339
// Fallback via libm, but the LLVM intrinsic is used instead.
338340
sym::sinf16, sym::sinf32, sym::sinf64,

compiler/rustc_hir_analysis/src/check/intrinsic.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ fn intrinsic_operation_unsafety(tcx: TyCtxt<'_>, intrinsic_id: LocalDefId) -> hi
135135
| sym::frem_algebraic
136136
| sym::fsub_algebraic
137137
| sym::gpu_launch_sized_workgroup_mem
138+
| sym::integer_max
139+
| sym::integer_min
138140
| sym::is_val_statically_known
139141
| sym::log2f16
140142
| sym::log2f32
@@ -602,6 +604,7 @@ pub(crate) fn check_intrinsic_type(
602604
vec![Ty::new_imm_ptr(tcx, param(0)), Ty::new_imm_ptr(tcx, param(0))],
603605
tcx.types.usize,
604606
),
607+
sym::integer_max | sym::integer_min => (1, 0, vec![param(0), param(0)], param(0)),
605608
sym::unchecked_div | sym::unchecked_rem | sym::exact_div | sym::disjoint_bitor => {
606609
(1, 0, vec![param(0), param(0)], param(0))
607610
}

compiler/rustc_span/src/symbol.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,8 @@ symbols! {
11451145
instruction_set,
11461146
instrument_fn,
11471147
integer_: "integer", // underscore to avoid clashing with the function `sym::integer` below
1148+
integer_max,
1149+
integer_min,
11481150
integral,
11491151
internal,
11501152
internal_eq_trait_method_impls,

library/core/src/cmp.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2298,8 +2298,37 @@ mod impls {
22982298

22992299
partial_ord_impl! { f16 f32 f64 f128 }
23002300

2301+
macro_rules! min_max_impl {
2302+
(char) => {
2303+
#[inline]
2304+
fn min(self, other: Self) -> Self {
2305+
let c = u32::min(self as u32, other as u32);
2306+
// SAFETY: it's one of the inputs
2307+
unsafe { char::from_u32_unchecked(c) }
2308+
}
2309+
2310+
#[inline]
2311+
fn max(self, other: Self) -> Self {
2312+
let c = u32::max(self as u32, other as u32);
2313+
// SAFETY: it's one of the inputs
2314+
unsafe { char::from_u32_unchecked(c) }
2315+
}
2316+
};
2317+
($t:ident) => {
2318+
#[inline]
2319+
fn min(self, other: Self) -> Self {
2320+
crate::intrinsics::integer_min(self, other)
2321+
}
2322+
2323+
#[inline]
2324+
fn max(self, other: Self) -> Self {
2325+
crate::intrinsics::integer_max(self, other)
2326+
}
2327+
};
2328+
}
2329+
23012330
macro_rules! ord_impl {
2302-
($($t:ty)*) => ($(
2331+
($($t:ident)*) => ($(
23032332
#[stable(feature = "rust1", since = "1.0.0")]
23042333
#[rustc_const_unstable(feature = "const_cmp", issue = "143800")]
23052334
const impl PartialOrd for $t {
@@ -2338,6 +2367,8 @@ mod impls {
23382367
self
23392368
}
23402369
}
2370+
2371+
min_max_impl!($t);
23412372
}
23422373
)*)
23432374
}

library/core/src/intrinsics/bounds.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,21 @@ const unsafe impl FloatPrimitive for f128 {
109109
f128::from_bits(bits)
110110
}
111111
}
112+
113+
/// Built-in integer types (i8, i16, .., i128, isize, u8, u16, .., u128, usize).
114+
///
115+
/// Intentionally does not include other integer-repr types like `bool` or `char`.
116+
///
117+
/// # Safety
118+
/// Must actually *be* such a type.
119+
#[rustc_const_unstable(feature = "core_intrinsics", issue = "none")]
120+
pub const unsafe trait IntegerPrimitive: Copy + [const] Ord {}
121+
122+
macro_rules! impl_integer_primitive {
123+
($($t:ty),*) => {$(
124+
#[rustc_const_unstable(feature = "core_intrinsics", issue = "none")]
125+
const unsafe impl IntegerPrimitive for $t {}
126+
)*};
127+
}
128+
impl_integer_primitive!(i8, i16, i32, i64, i128, isize);
129+
impl_integer_primitive!(u8, u16, u32, u64, u128, usize);

library/core/src/intrinsics/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,6 +1841,34 @@ pub const fn fdiv_algebraic<T: bounds::FloatPrimitive>(a: T, b: T) -> T;
18411841
#[rustc_intrinsic]
18421842
pub const fn frem_algebraic<T: bounds::FloatPrimitive>(a: T, b: T) -> T;
18431843

1844+
/// Integer `min`imum, signed or unsigned depending on `T`.
1845+
///
1846+
/// Allowed only on `uN`, `iN`, `usize`, and `isize`.
1847+
/// (Not on `bool` nor on `char`.)
1848+
///
1849+
/// Stabilized as [`u16::min`] and [`i64::min`] and similar.
1850+
#[rustc_const_unstable(feature = "const_cmp", issue = "143800")]
1851+
#[rustc_nounwind]
1852+
#[rustc_intrinsic]
1853+
#[miri::intrinsic_fallback_is_spec]
1854+
pub const fn integer_min<T: [const] bounds::IntegerPrimitive>(a: T, b: T) -> T {
1855+
if a < b { a } else { b }
1856+
}
1857+
1858+
/// Integer `max`imum, signed or unsigned depending on `T`.
1859+
///
1860+
/// Allowed only on `uN`, `iN`, `usize`, and `isize`.
1861+
/// (Not on `bool` nor on `char`.)
1862+
///
1863+
/// Stabilized as [`u16::max`] and [`i64::max`] and similar.
1864+
#[rustc_const_unstable(feature = "const_cmp", issue = "143800")]
1865+
#[rustc_nounwind]
1866+
#[rustc_intrinsic]
1867+
#[miri::intrinsic_fallback_is_spec]
1868+
pub const fn integer_max<T: [const] bounds::IntegerPrimitive>(a: T, b: T) -> T {
1869+
if a < b { b } else { a }
1870+
}
1871+
18441872
/// Returns the number of bits set in an integer type `T`
18451873
///
18461874
/// Note that, unlike most intrinsics, this is safe to call;

library/coretests/tests/cmp.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ fn test_mut_int_totalord() {
2727
assert_eq!((&mut 12).cmp(&&mut -5), Greater);
2828
}
2929

30+
#[test]
31+
fn test_max_min_signedness() {
32+
use std::cmp::{max, min};
33+
// Check the "same" 8-bit values where the signedness of the operation matters
34+
assert_eq!(max::<u8>(0, 255), 255);
35+
assert_eq!(max::<u8>(255, 0), 255);
36+
assert_eq!(min::<u8>(0, 255), 0);
37+
assert_eq!(min::<u8>(255, 0), 0);
38+
assert_eq!(max::<i8>(0, -1), 0);
39+
assert_eq!(max::<i8>(-1, 0), 0);
40+
assert_eq!(min::<i8>(0, -1), -1);
41+
assert_eq!(min::<i8>(-1, 0), -1);
42+
}
43+
3044
#[test]
3145
fn test_ord_max_min() {
3246
assert_eq!(1.max(2), 2);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//@ compile-flags: -C opt-level=3 -C no-prepopulate-passes
2+
3+
#![crate_type = "lib"]
4+
5+
#[unsafe(no_mangle)]
6+
pub fn i16_min(a: i16, b: i16) -> i16 {
7+
// CHECK-LABEL: i16_min
8+
// CHECK: [[M:%.+]] = call i16 @llvm.smin.i16(i16 %a, i16 %b)
9+
// CHECK-NEXT: ret i16 [[M]]
10+
std::cmp::min(a, b)
11+
}
12+
13+
#[unsafe(no_mangle)]
14+
pub fn i32_max(a: i32, b: i32) -> i32 {
15+
// CHECK-LABEL: i32_max
16+
// CHECK: [[M:%.+]] = call i32 @llvm.smax.i32(i32 %a, i32 %b)
17+
// CHECK-NEXT: ret i32 [[M]]
18+
std::cmp::max(a, b)
19+
}
20+
21+
#[unsafe(no_mangle)]
22+
pub fn u8_min(a: u8, b: u8) -> u8 {
23+
// CHECK-LABEL: u8_min
24+
// CHECK: [[M:%.+]] = call i8 @llvm.umin.i8(i8 %a, i8 %b)
25+
// CHECK-NEXT: ret i8 [[M]]
26+
std::cmp::min(a, b)
27+
}
28+
29+
#[unsafe(no_mangle)]
30+
pub fn u16_max(a: u16, b: u16) -> u16 {
31+
// CHECK-LABEL: u16_max
32+
// CHECK: [[M:%.+]] = call i16 @llvm.umax.i16(i16 %a, i16 %b)
33+
// CHECK-NEXT: ret i16 [[M]]
34+
std::cmp::max(a, b)
35+
}
36+
37+
#[unsafe(no_mangle)]
38+
pub fn char_min(a: char, b: char) -> char {
39+
// CHECK-LABEL: char_min
40+
// CHECK: [[M:%.+]] = call i32 @llvm.umin.i32(i32 %a, i32 %b)
41+
// CHECK: ret i32 [[M]]
42+
std::cmp::min(a, b)
43+
}
44+
45+
#[unsafe(no_mangle)]
46+
pub fn char_max(a: char, b: char) -> char {
47+
// CHECK-LABEL: char_max
48+
// CHECK: [[M:%.+]] = call i32 @llvm.umax.i32(i32 %a, i32 %b)
49+
// CHECK: ret i32 [[M]]
50+
std::cmp::max(a, b)
51+
}

0 commit comments

Comments
 (0)