-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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
Fix #[inline(always)] on closures with target feature 1.1 #111836
Conversation
r? @davidtwco (rustbot has picked a reviewer for you, use r? to override) |
@calebzulawski we can re-roll reviewer if you'd like 🙂 |
I've gotten rid of my other hundreds of notifications, I can work on this finally. |
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.
I know I basically suggested it but now that I have a good look at the code and am a few months of thinking about target features wiser, I'm concerned about the behavior of this approach for this particular, admittedly somewhat contrived example:
#![feature(target_feature_11)]
use core::arch::x86_64::*;
#[target_feature(enable = "avx")]
pub unsafe fn escape(a: f64, b: f64, c: f64, d: f64) -> impl Fn() -> __m256d {
#[inline(always)]
move || _mm256_set_pd(a, b, c, d)
}
#[target_feature(enable = "avx")]
pub unsafe fn way_out() -> fn(__m256d) -> i32 {
#[inline(always)]
move |a| _mm256_movemask_pd(a)
}
pub fn unsafe_haven(a: f64, b: f64, c: f64, d: f64) -> i32 {
// Problem: Even though this code declared
// that it met escape()'s and way_out()'s unsafe preconditions,
// THIS function doesn't have the target features!
let escapee = unsafe { escape(a, b, c, d) };
let escaping_avx_type = escapee();
let opening = unsafe { way_out() };
opening(escaping_avx_type)
}
#[inline(always)] | ||
move || {} |
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.
Thinking about this forced me to check if you can annotate closures with target_feature(enable)
. (You cannot, fortunately.)
// would result in this closure being compiled without the inherited target features, but this | ||
// is probably a poor usage of `#[inline(always)]` and easily avoided by not using the attribute. | ||
if tcx.features().target_feature_11 | ||
&& tcx.is_closure(did.to_def_id()) |
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.
...apparently is_closure
will return true
if this is a generator, also. I frankly have no idea how that should work, but dropping the features should remain safe in that case, at least...
// its parent function, which effectively inherits the features anyway. Boxing this closure | ||
// would result in this closure being compiled without the inherited target features, but this | ||
// is probably a poor usage of `#[inline(always)]` and easily avoided by not using the attribute. |
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.
Boxing seems like a waste, yes, but now that I am thinking about it, this seems like it could result in confusing behavior in the "escaping closure" case, when that would result, instead of the IIFE? Does that even make sense?
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.
Inlining with closures is unfortunately always confusing. Box<dyn FnOnce()>
, for example, implements FnOnce
itself:
rust/library/alloc/src/boxed.rs
Lines 2003 to 2009 in c4083fa
impl<Args: Tuple, F: FnOnce<Args> + ?Sized, A: Allocator> FnOnce<Args> for Box<F, A> { | |
type Output = <F as FnOnce<Args>>::Output; | |
extern "rust-call" fn call_once(self, args: Args) -> Self::Output { | |
<F as FnOnce<Args>>::call_once(*self, args) | |
} | |
} |
This call_once
doesn't have any inline attribute at all! Therefore, the boxed closure's call_once
inlines into this call_once
, and then it's up in the air after that.
I don't think there is actually anything wrong here. The |
Yes, I'm handwaving feature detection for this example. Technically it's not unsound until someone actually calls it. :^) The "ultimate" question seems to be if this is truly preferable over demoting the Currently, the Relevant issues and commits: |
I'm not sure if Regardless, I think this behavior is probably best for now because I'm sure there is existing code with |
It is because we say it is, as I understand it. LLVM is allowed to choose to not error on that case and simply silently ignore it, and as I understand it has in the past, and as you observed, it only applies to direct calls. I guess the matter of indirection is most of what's really becoming pertinent, now that I think about it: If we do this, then "featureful inlining" stops before the closure, but if we don't, it continues into the closure, but the closure itself may not get inlined. So if there is some reason that the closure's exterior gets "outlined" anyways, like the I might be wrong, obviously. However, one thing I am confident about is that we should not have to guess: This needs, at minimum, codegen tests in order to validate the LLVMIR is what we expect for both the direct call and indirect call cases, and we're going to need enough nesting that we can see all the consequences. This will help clarify what LLVM actually does, illuminate which approaches might actually lead to performance regressions, and catch whether LLVM decides to change its mind. |
I recommend making a There is no way this is going to be the last of these. |
To take a step back for a moment, extending That said, like the added comment indicates, using I'm basically saying it's not worth overthinking it. I'm confident this change won't do anything unsound, it might not have completely optimal codegen in unusual edge cases, but I think it's easy to work around. At worst, this behavior could be adjusted in a follow up PR, since it's just codegen and not language semantics :) |
I agree (re: "At worst, this behavior could be adjusted in a follow up PR, since it's just codegen"), I just still want to see codegen tests so that if LLVM changes their inlining rules again for target features we can catch it. :^) |
@rustbot author |
All tests good 🙂 |
Let's give this a whirl. @bors r+ rollup=never |
☀️ Test successful - checks-actions |
Finished benchmarking commit (1c44af9): comparison URL. Overall result: no relevant changes - no action needed@rustbot label: -perf-regression Instruction countThis benchmark run did not return any relevant results for this metric. Max RSS (memory usage)This benchmark run did not return any relevant results for this metric. CyclesResultsThis is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Binary sizeThis benchmark run did not return any relevant results for this metric. Bootstrap: 651.172s -> 651.296s (0.02%) |
Stabilize target_feature_11 # Stabilization report This is an updated version of rust-lang#116114, which is itself a redo of rust-lang#99767. Most of this commit and report were copied from those PRs. Thanks `@LeSeulArtichaut` and `@calebzulawski!` ## Summary Allows for safe functions to be marked with `#[target_feature]` attributes. Functions marked with `#[target_feature]` are generally considered as unsafe functions: they are unsafe to call, cannot *generally* be assigned to safe function pointers, and don't implement the `Fn*` traits. However, calling them from other `#[target_feature]` functions with a superset of features is safe. ```rust // Demonstration function #[target_feature(enable = "avx2")] fn avx2() {} fn foo() { // Calling `avx2` here is unsafe, as we must ensure // that AVX is available first. unsafe { avx2(); } } #[target_feature(enable = "avx2")] fn bar() { // Calling `avx2` here is safe. avx2(); } ``` Moreover, once rust-lang#135504 is merged, they can be converted to safe function pointers in a context in which calling them is safe: ```rust // Demonstration function #[target_feature(enable = "avx2")] fn avx2() {} fn foo() -> fn() { // Converting `avx2` to fn() is a compilation error here. avx2 } #[target_feature(enable = "avx2")] fn bar() -> fn() { // `avx2` coerces to fn() here avx2 } ``` See the section "Closures" below for justification of this behaviour. ## Test cases Tests for this feature can be found in [`tests/ui/target_feature/`](https://github.com/rust-lang/rust/tree/f6cb952dc115fd1311b02b694933e31d8dc8b002/tests/ui/target-feature). ## Edge cases ### Closures * [target-feature 1.1: should closures inherit target-feature annotations? rust-lang#73631](rust-lang#73631) Closures defined inside functions marked with #[target_feature] inherit the target features of their parent function. They can still be assigned to safe function pointers and implement the appropriate `Fn*` traits. ```rust #[target_feature(enable = "avx2")] fn qux() { let my_closure = || avx2(); // this call to `avx2` is safe let f: fn() = my_closure; } ``` This means that in order to call a function with #[target_feature], you must guarantee that the target-feature is available while the function, any closures defined inside it, as well as any safe function pointers obtained from target-feature functions inside it, execute. This is usually ensured because target features are assumed to never disappear, and: - on any unsafe call to a `#[target_feature]` function, presence of the target feature is guaranteed by the programmer through the safety requirements of the unsafe call. - on any safe call, this is guaranteed recursively by the caller. If you work in an environment where target features can be disabled, it is your responsibility to ensure that no code inside a target feature function (including inside a closure) runs after this (until the feature is enabled again). **Note:** this has an effect on existing code, as nowadays closures do not inherit features from the enclosing function, and thus this strengthens a safety requirement. It was originally proposed in rust-lang#73631 to solve this by adding a new type of UB: “taking a target feature away from your process after having run code that uses that target feature is UB” . This was motivated by userspace code already assuming in a few places that CPU features never disappear from a program during execution (see i.e. https://github.com/rust-lang/stdarch/blob/2e29bdf90832931ea499755bb4ad7a6b0809295a/crates/std_detect/src/detect/arch/x86.rs); however, concerns were raised in the context of the Linux kernel; thus, we propose to relax that requirement to "causing the set of usable features to be reduced is unsafe; when doing so, the programmer is required to ensure that no closures or safe fn pointers that use removed features are still in scope". * [Fix #[inline(always)] on closures with target feature 1.1 rust-lang#111836](rust-lang#111836) Closures accept `#[inline(always)]`, even within functions marked with `#[target_feature]`. Since these attributes conflict, `#[inline(always)]` wins out to maintain compatibility. ### ABI concerns * [The extern "C" ABI of SIMD vector types depends on target features rust-lang#116558](rust-lang#116558) The ABI of some types can change when compiling a function with different target features. This could have introduced unsoundness with target_feature_11, but recent fixes (rust-lang#133102, rust-lang#132173) either make those situations invalid or make the ABI no longer dependent on features. Thus, those issues should no longer occur. ### Special functions The `#[target_feature]` attribute is forbidden from a variety of special functions, such as main, current and future lang items (e.g. `#[start]`, `#[panic_handler]`), safe default trait implementations and safe trait methods. This was not disallowed at the time of the first stabilization PR for target_features_11, and resulted in the following issues/PRs: * [`#[target_feature]` is allowed on `main` rust-lang#108645](rust-lang#108645) * [`#[target_feature]` is allowed on default implementations rust-lang#108646](rust-lang#108646) * [#[target_feature] is allowed on #[panic_handler] with target_feature 1.1 rust-lang#109411](rust-lang#109411) * [Prevent using `#[target_feature]` on lang item functions rust-lang#115910](rust-lang#115910) ## Documentation * Reference: [Document the `target_feature_11` feature reference#1181](rust-lang/reference#1181) --- cc tracking issue rust-lang#69098 cc `@workingjubilee` cc `@RalfJung` r? `@rust-lang/lang`
Fixes #108655. I think this is the most obvious solution that isn't overly complicated. The comment includes more justification, but I think this is likely better than demoting the
#[inline(always)]
to#[inline]
, since existing code is unaffected.