-
Notifications
You must be signed in to change notification settings - Fork 13.6k
Simplify discriminant codegen for niche-encoded variants which don't wrap across an integer boundary #143784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+669
−40
Merged
Simplify discriminant codegen for niche-encoded variants which don't wrap across an integer boundary #143784
Changes from 2 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -486,6 +486,7 @@ impl<'a, 'tcx, V: CodegenObject> OperandRef<'tcx, V> { | |
// value and the variant index match, since that's all `Niche` can encode. | ||
|
||
let relative_max = niche_variants.end().as_u32() - niche_variants.start().as_u32(); | ||
let niche_start_const = bx.cx().const_uint_big(tag_llty, niche_start); | ||
|
||
// We have a subrange `niche_start..=niche_end` inside `range`. | ||
// If the value of the tag is inside this subrange, it's a | ||
|
@@ -511,35 +512,44 @@ impl<'a, 'tcx, V: CodegenObject> OperandRef<'tcx, V> { | |
// } else { | ||
// untagged_variant | ||
// } | ||
let niche_start = bx.cx().const_uint_big(tag_llty, niche_start); | ||
let is_niche = bx.icmp(IntPredicate::IntEQ, tag, niche_start); | ||
let is_niche = bx.icmp(IntPredicate::IntEQ, tag, niche_start_const); | ||
let tagged_discr = | ||
bx.cx().const_uint(cast_to, niche_variants.start().as_u32() as u64); | ||
(is_niche, tagged_discr, 0) | ||
} else { | ||
// The special cases don't apply, so we'll have to go with | ||
// the general algorithm. | ||
let relative_discr = bx.sub(tag, bx.cx().const_uint_big(tag_llty, niche_start)); | ||
|
||
let tag_range = tag_scalar.valid_range(&dl); | ||
let tag_size = tag_scalar.size(&dl); | ||
let niche_end = u128::from(relative_max).wrapping_add(niche_start); | ||
let niche_end = tag_size.truncate(niche_end); | ||
|
||
let relative_discr = bx.sub(tag, niche_start_const); | ||
let cast_tag = bx.intcast(relative_discr, cast_to, false); | ||
let is_niche = bx.icmp( | ||
IntPredicate::IntULE, | ||
relative_discr, | ||
bx.cx().const_uint(tag_llty, relative_max as u64), | ||
); | ||
|
||
// Thanks to parameter attributes and load metadata, LLVM already knows | ||
// the general valid range of the tag. It's possible, though, for there | ||
// to be an impossible value *in the middle*, which those ranges don't | ||
// communicate, so it's worth an `assume` to let the optimizer know. | ||
if niche_variants.contains(&untagged_variant) | ||
&& bx.cx().sess().opts.optimize != OptLevel::No | ||
{ | ||
let impossible = | ||
u64::from(untagged_variant.as_u32() - niche_variants.start().as_u32()); | ||
let impossible = bx.cx().const_uint(tag_llty, impossible); | ||
let ne = bx.icmp(IntPredicate::IntNE, relative_discr, impossible); | ||
bx.assume(ne); | ||
} | ||
let is_niche = if tag_range.no_unsigned_wraparound(tag_size) == Ok(true) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add some description of the special case here? Does a diagram like this make sense?
|
||
if niche_start == tag_range.start { | ||
let niche_end_const = bx.cx().const_uint_big(tag_llty, niche_end); | ||
bx.icmp(IntPredicate::IntULE, tag, niche_end_const) | ||
} else { | ||
assert_eq!(niche_end, tag_range.end); | ||
bx.icmp(IntPredicate::IntUGE, tag, niche_start_const) | ||
} | ||
} else if tag_range.no_signed_wraparound(tag_size) == Ok(true) { | ||
if niche_start == tag_range.start { | ||
let niche_end_const = bx.cx().const_uint_big(tag_llty, niche_end); | ||
bx.icmp(IntPredicate::IntSLE, tag, niche_end_const) | ||
} else { | ||
assert_eq!(niche_end, tag_range.end); | ||
bx.icmp(IntPredicate::IntSGE, tag, niche_start_const) | ||
} | ||
} else { | ||
bx.icmp( | ||
IntPredicate::IntULE, | ||
relative_discr, | ||
bx.cx().const_uint(tag_llty, relative_max as u64), | ||
) | ||
}; | ||
|
||
(is_niche, cast_tag, niche_variants.start().as_u32() as u128) | ||
}; | ||
|
@@ -550,11 +560,24 @@ impl<'a, 'tcx, V: CodegenObject> OperandRef<'tcx, V> { | |
bx.add(tagged_discr, bx.cx().const_uint_big(cast_to, delta)) | ||
}; | ||
|
||
let discr = bx.select( | ||
is_niche, | ||
tagged_discr, | ||
bx.cx().const_uint(cast_to, untagged_variant.as_u32() as u64), | ||
); | ||
let untagged_variant_const = | ||
bx.cx().const_uint(cast_to, u64::from(untagged_variant.as_u32())); | ||
|
||
// Thanks to parameter attributes and load metadata, LLVM already knows | ||
// the general valid range of the tag. It's possible, though, for there | ||
// to be an impossible value *in the middle*, which those ranges don't | ||
// communicate, so it's worth an `assume` to let the optimizer know. | ||
// Most importantly, this means when optimizing a variant test like | ||
// `SELECT(is_niche, complex, CONST) == CONST` it's ok to simplify that | ||
// to `!is_niche` because the `complex` part can't possibly match. | ||
if niche_variants.contains(&untagged_variant) | ||
&& bx.cx().sess().opts.optimize != OptLevel::No | ||
{ | ||
let ne = bx.icmp(IntPredicate::IntNE, tagged_discr, untagged_variant_const); | ||
bx.assume(ne); | ||
} | ||
|
||
let discr = bx.select(is_niche, tagged_discr, untagged_variant_const); | ||
|
||
// In principle we could insert assumes on the possible range of `discr`, but | ||
// currently in LLVM this isn't worth it because the original `tag` will | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
//@ compile-flags: -Copt-level=3 -Zmerge-functions=disabled | ||
//@ min-llvm-version: 20 | ||
//@ only-64bit | ||
|
||
// The `derive(PartialEq)` on enums with field-less variants compares discriminants, | ||
// so make sure we emit that in some reasonable way. | ||
|
||
#![crate_type = "lib"] | ||
#![feature(ascii_char)] | ||
#![feature(core_intrinsics)] | ||
#![feature(repr128)] | ||
|
||
use std::ascii::Char as AC; | ||
use std::cmp::Ordering; | ||
use std::intrinsics::discriminant_value; | ||
use std::num::NonZero; | ||
|
||
// A type that's bigger than `isize`, unlike the usual cases that have small tags. | ||
#[repr(u128)] | ||
pub enum Giant { | ||
Two = 2, | ||
Three = 3, | ||
Four = 4, | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn opt_bool_eq_discr(a: Option<bool>, b: Option<bool>) -> bool { | ||
// CHECK-LABEL: @opt_bool_eq_discr( | ||
// CHECK: %[[A:.+]] = icmp ne i8 %a, 2 | ||
// CHECK: %[[B:.+]] = icmp eq i8 %b, 2 | ||
// CHECK: %[[R:.+]] = xor i1 %[[A]], %[[B]] | ||
// CHECK: ret i1 %[[R]] | ||
|
||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn opt_ord_eq_discr(a: Option<Ordering>, b: Option<Ordering>) -> bool { | ||
// CHECK-LABEL: @opt_ord_eq_discr( | ||
// CHECK: %[[A:.+]] = icmp ne i8 %a, 2 | ||
// CHECK: %[[B:.+]] = icmp eq i8 %b, 2 | ||
// CHECK: %[[R:.+]] = xor i1 %[[A]], %[[B]] | ||
// CHECK: ret i1 %[[R]] | ||
|
||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn opt_nz32_eq_discr(a: Option<NonZero<u32>>, b: Option<NonZero<u32>>) -> bool { | ||
// CHECK-LABEL: @opt_nz32_eq_discr( | ||
// CHECK: %[[A:.+]] = icmp ne i32 %a, 0 | ||
// CHECK: %[[B:.+]] = icmp eq i32 %b, 0 | ||
// CHECK: %[[R:.+]] = xor i1 %[[A]], %[[B]] | ||
// CHECK: ret i1 %[[R]] | ||
|
||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn opt_ac_eq_discr(a: Option<AC>, b: Option<AC>) -> bool { | ||
// CHECK-LABEL: @opt_ac_eq_discr( | ||
// CHECK: %[[A:.+]] = icmp ne i8 %a, -128 | ||
// CHECK: %[[B:.+]] = icmp eq i8 %b, -128 | ||
// CHECK: %[[R:.+]] = xor i1 %[[A]], %[[B]] | ||
// CHECK: ret i1 %[[R]] | ||
|
||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn opt_giant_eq_discr(a: Option<Giant>, b: Option<Giant>) -> bool { | ||
// CHECK-LABEL: @opt_giant_eq_discr( | ||
// CHECK: %[[A:.+]] = icmp ne i128 %a, 1 | ||
// CHECK: %[[B:.+]] = icmp eq i128 %b, 1 | ||
// CHECK: %[[R:.+]] = xor i1 %[[A]], %[[B]] | ||
// CHECK: ret i1 %[[R]] | ||
|
||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
pub enum Mid<T> { | ||
Before, | ||
Thing(T), | ||
After, | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_bool_eq_discr(a: Mid<bool>, b: Mid<bool>) -> bool { | ||
// CHECK-LABEL: @mid_bool_eq_discr( | ||
|
||
// CHECK: %[[A_REL_DISCR:.+]] = add nsw i8 %a, -2 | ||
// CHECK: %[[A_IS_NICHE:.+]] = icmp samesign ugt i8 %a, 1 | ||
// CHECK: %[[A_NOT_HOLE:.+]] = icmp ne i8 %[[A_REL_DISCR]], 1 | ||
// CHECK: tail call void @llvm.assume(i1 %[[A_NOT_HOLE]]) | ||
// CHECK: %[[A_DISCR:.+]] = select i1 %[[A_IS_NICHE]], i8 %[[A_REL_DISCR]], i8 1 | ||
|
||
// CHECK: %[[B_REL_DISCR:.+]] = add nsw i8 %b, -2 | ||
// CHECK: %[[B_IS_NICHE:.+]] = icmp samesign ugt i8 %b, 1 | ||
// CHECK: %[[B_NOT_HOLE:.+]] = icmp ne i8 %[[B_REL_DISCR]], 1 | ||
// CHECK: tail call void @llvm.assume(i1 %[[B_NOT_HOLE]]) | ||
// CHECK: %[[B_DISCR:.+]] = select i1 %[[B_IS_NICHE]], i8 %[[B_REL_DISCR]], i8 1 | ||
|
||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_ord_eq_discr(a: Mid<Ordering>, b: Mid<Ordering>) -> bool { | ||
// CHECK-LABEL: @mid_ord_eq_discr( | ||
|
||
// CHECK: %[[A_REL_DISCR:.+]] = add nsw i8 %a, -2 | ||
// CHECK: %[[A_IS_NICHE:.+]] = icmp sgt i8 %a, 1 | ||
// CHECK: %[[A_NOT_HOLE:.+]] = icmp ne i8 %[[A_REL_DISCR]], 1 | ||
// CHECK: tail call void @llvm.assume(i1 %[[A_NOT_HOLE]]) | ||
// CHECK: %[[A_DISCR:.+]] = select i1 %[[A_IS_NICHE]], i8 %[[A_REL_DISCR]], i8 1 | ||
|
||
// CHECK: %[[B_REL_DISCR:.+]] = add nsw i8 %b, -2 | ||
// CHECK: %[[B_IS_NICHE:.+]] = icmp sgt i8 %b, 1 | ||
// CHECK: %[[B_NOT_HOLE:.+]] = icmp ne i8 %[[B_REL_DISCR]], 1 | ||
// CHECK: tail call void @llvm.assume(i1 %[[B_NOT_HOLE]]) | ||
// CHECK: %[[B_DISCR:.+]] = select i1 %[[B_IS_NICHE]], i8 %[[B_REL_DISCR]], i8 1 | ||
|
||
// CHECK: %[[R:.+]] = icmp eq i8 %[[A_DISCR]], %[[B_DISCR]] | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_nz32_eq_discr(a: Mid<NonZero<u32>>, b: Mid<NonZero<u32>>) -> bool { | ||
// CHECK-LABEL: @mid_nz32_eq_discr( | ||
// CHECK: %[[R:.+]] = icmp eq i32 %a.0, %b.0 | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_ac_eq_discr(a: Mid<AC>, b: Mid<AC>) -> bool { | ||
// CHECK-LABEL: @mid_ac_eq_discr( | ||
|
||
// CHECK: %[[A_REL_DISCR:.+]] = xor i8 %a, -128 | ||
// CHECK: %[[A_IS_NICHE:.+]] = icmp slt i8 %a, 0 | ||
// CHECK: %[[A_NOT_HOLE:.+]] = icmp ne i8 %a, -127 | ||
// CHECK: tail call void @llvm.assume(i1 %[[A_NOT_HOLE]]) | ||
// CHECK: %[[A_DISCR:.+]] = select i1 %[[A_IS_NICHE]], i8 %[[A_REL_DISCR]], i8 1 | ||
|
||
// CHECK: %[[B_REL_DISCR:.+]] = xor i8 %b, -128 | ||
// CHECK: %[[B_IS_NICHE:.+]] = icmp slt i8 %b, 0 | ||
// CHECK: %[[B_NOT_HOLE:.+]] = icmp ne i8 %b, -127 | ||
// CHECK: tail call void @llvm.assume(i1 %[[B_NOT_HOLE]]) | ||
// CHECK: %[[B_DISCR:.+]] = select i1 %[[B_IS_NICHE]], i8 %[[B_REL_DISCR]], i8 1 | ||
|
||
// CHECK: %[[R:.+]] = icmp eq i8 %[[A_DISCR]], %[[B_DISCR]] | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
// FIXME: This should be improved once our LLVM fork picks up the fix for | ||
// <https://github.com/llvm/llvm-project/issues/134024> | ||
#[unsafe(no_mangle)] | ||
pub fn mid_giant_eq_discr(a: Mid<Giant>, b: Mid<Giant>) -> bool { | ||
// CHECK-LABEL: @mid_giant_eq_discr( | ||
|
||
// CHECK: %[[A_TRUNC:.+]] = trunc nuw nsw i128 %a to i64 | ||
// CHECK: %[[A_REL_DISCR:.+]] = add nsw i64 %[[A_TRUNC]], -5 | ||
// CHECK: %[[A_IS_NICHE:.+]] = icmp samesign ugt i128 %a, 4 | ||
// CHECK: %[[A_NOT_HOLE:.+]] = icmp ne i64 %[[A_REL_DISCR]], 1 | ||
// CHECK: tail call void @llvm.assume(i1 %[[A_NOT_HOLE]]) | ||
// CHECK: %[[A_DISCR:.+]] = select i1 %[[A_IS_NICHE]], i64 %[[A_REL_DISCR]], i64 1 | ||
|
||
// CHECK: %[[B_TRUNC:.+]] = trunc nuw nsw i128 %b to i64 | ||
// CHECK: %[[B_REL_DISCR:.+]] = add nsw i64 %[[B_TRUNC]], -5 | ||
// CHECK: %[[B_IS_NICHE:.+]] = icmp samesign ugt i128 %b, 4 | ||
// CHECK: %[[B_NOT_HOLE:.+]] = icmp ne i64 %[[B_REL_DISCR]], 1 | ||
// CHECK: tail call void @llvm.assume(i1 %[[B_NOT_HOLE]]) | ||
// CHECK: %[[B_DISCR:.+]] = select i1 %[[B_IS_NICHE]], i64 %[[B_REL_DISCR]], i64 1 | ||
|
||
// CHECK: %[[R:.+]] = icmp eq i64 %[[A_DISCR]], %[[B_DISCR]] | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == discriminant_value(&b) | ||
} | ||
|
||
// In niche-encoded enums, testing for the untagged variant should optimize to a | ||
// straight-forward comparison looking for the natural range of the payload value. | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_bool_is_thing(a: Mid<bool>) -> bool { | ||
// CHECK-LABEL: @mid_bool_is_thing( | ||
// CHECK: %[[R:.+]] = icmp samesign ult i8 %a, 2 | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == 1 | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_ord_is_thing(a: Mid<Ordering>) -> bool { | ||
// CHECK-LABEL: @mid_ord_is_thing( | ||
// CHECK: %[[R:.+]] = icmp slt i8 %a, 2 | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == 1 | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_nz32_is_thing(a: Mid<NonZero<u32>>) -> bool { | ||
// CHECK-LABEL: @mid_nz32_is_thing( | ||
// CHECK: %[[R:.+]] = icmp eq i32 %a.0, 1 | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == 1 | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_ac_is_thing(a: Mid<AC>) -> bool { | ||
// CHECK-LABEL: @mid_ac_is_thing( | ||
// CHECK: %[[R:.+]] = icmp sgt i8 %a, -1 | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == 1 | ||
} | ||
|
||
#[unsafe(no_mangle)] | ||
pub fn mid_giant_is_thing(a: Mid<Giant>) -> bool { | ||
// CHECK-LABEL: @mid_giant_is_thing( | ||
// CHECK: %[[R:.+]] = icmp samesign ult i128 %a, 5 | ||
// CHECK: ret i1 %[[R]] | ||
discriminant_value(&a) == 1 | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the comment be repositioned?