Refactor core::cmp::{smallest, largest} & add mir-opt test - #161275
Refactor core::cmp::{smallest, largest} & add mir-opt test#161275bushrat011899 wants to merge 2 commits into
core::cmp::{smallest, largest} & add mir-opt test#161275Conversation
Remove support for 1 argument variant and adjust implementation to reduce MIR generated.
2-argument variant in release mode reduces down to the same function regardless, but the debug build is affected.
|
rustbot has assigned @JonathanBrouwer. Use Why was this reviewer chosen?The reviewer was selected based on:
|
|
r? libs |
I'm wondering if it would be easier/better to show this on -Cno-prepopulate-passes LLVM IR? That should be able to do it without duplicating core's impl, as long as the impls in core are cc @scottmcm Looking at the actual impl here, I guess the reason we're pulling out two tuple 'parts' is to simplify the common case of having smallest/largest with just two arguments passed? I'm wondering if we'd get better codegen in debug mode if we made this more explicitly a loop, by transmuting the (T, T, ...) into [T; N] and then going through Iterator::{min, max}? I'm pretty sure we don't guarantee enough about repr(Rust) to make that transmute always compile, but if it does, then I think it is a sound way to access the elements? In particular I suspect doing it via min/max on the array makes it easier for LLVM to notice the loop and not lower to "forcefully" unrolled code... while hopefully not making the common (short) case much worse. |
There was a problem hiding this comment.
I think what we probably want here is a pre-codegen MIR test: https://github.com/rust-lang/rust/tree/main/tests/mir-opt/pre-codegen
Notably, those do run mir optimizations, so it should be fine to call cmp::min and cmp::smallest and see what they do. (Which, after #161081, should both just be "call the compiler intrinsic.)
If open-coded smallest will have more overhead in unoptimized, but I'm not actually that worried about it. We always ship the sysroot with mir optimizations, for example.
I mostly just want to see that the various method layers properly fold away thanks to the mir-opt-level=2 when people call the std methods. (This is similar to the tests that make sure that passing tiny closures to Option::map can get inlined so it's not gratuitously different from a manual if-let.)
TBH it might not even need any changes to the implementation, since https://rust.godbolt.org/z/cGPqao9jf is already looking pretty reasonable. (That nightly doesn't have the min intrinsic, though, so you'll see in a test here will probably be different.)
|
Reminder, once the PR becomes ready for a review, use |
| v1: T, | ||
| v2: T, | ||
| #[rustc_splat] args: impl [const] SmallestArgs<T>, |
There was a problem hiding this comment.
I didn't know this worked with splat; cool!
My instinct would be to keep the 1-parameter version working, but to do the middle-ground here were it emphasizes that the 0-parameter version doesn't exist without understanding the trait impls. (If in future this could be #[rustc_splat] args: [T; N], for example, that'd be pretty sweet.)
So maybe something like
| v1: T, | |
| v2: T, | |
| #[rustc_splat] args: impl [const] SmallestArgs<T>, | |
| v: T, | |
| #[rustc_splat] args: impl [const] SmallestArgs<T>, |
instead?
| { | ||
| #[inline(always)] // improves unoptimised codegen | ||
| fn smallest(v1: T, v2: T, ($($x,)*): Self) -> T { | ||
| v1.min(v2)$(.min($x))* |
There was a problem hiding this comment.
...and having this be
| v1.min(v2)$(.min($x))* | |
| v $(.min($x))* |
without needing the separate detupling looks really elegant.
My instinct is that that'd be really really bad for the common cases, since we'd end up with the loop in MIR even for the 2-param case, which then wouldn't fold down (mir-opt doesn't have loop optimizations) and thus wouldn't be inlinable and would thus be overall worse. To me by the time you're passing the N args separately, it's fine (maybe even good) to forcibly expand to N calls. (Said otherwise, I think of this as "you don't have to write |
Tracking Issue: #160728
Description
Based on this feedback from the initial implementation, I've added a
mir-opttest to highlight the amount of MIR added by using a variadicminimplementation. To my knowledge, there isn't a way to actually test the MIR generated by the actual implementation incore, so I've replicated a smaller representative sample in a test instead. Please see GodBolt for a more detailed comparison between the (unoptimised) MIR for the current and splatted versions ofmin(maxomitted since it's identical).In short, the splat version does produce more MIR, especially for the additional trait implementations for larger tuple sizes. However, the added MIR almost exclusively comes from the indirection of calling a method on a trait, and not specifically the
#[rustc_splat]variadic implementation. Because of this, it may be worth considering a MIR pass which could inline this kind of static dispatch.From experimenting with this test, I also found a simpler way to implement
smallerandlarger. The most notable difference is instead of passing all arguments through the splat, I've pulled out the first two arguments so they aren't de/tupled. This does mean 1 argumentsmallest/largestno longer works, but I think that's actually preferable anyway, since it's just the identity function.Notes