-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Minimal target feature unsafe #2212
Conversation
The target feature RFC provides:
So maybe could you explain what you mean by "it's impossible to create efficient safe abstraction over the unsafe operations without cheating about the notion of safety."?
Does this mean that if you write A very common SIMD idiom is to, on program initialization, perform run-time feature detection, and set a global function pointer to the most efficient implementation of some algorithm. The target feature proposal allows this. If this proposal disallows it that would create an ecosystem split (e.g. I can't use some algorithm because somebody didn't wanted to annotate a target feature function with unsafe). |
I mean that it's not possible to safely wrap individual operations without also putting the check inside the wrapper, which isn't efficient. I'll amend the PR.
Yes, but you could still write
You'd have to make the entry point to the algorithm A function pointer erases information about set of instruction set extensions, so the set of instruction set extensions of the callee and the caller can't be compared statically by the compiler when a function is called through a function pointer. |
I guess, alternatively, taking a function pointer to a safe function that has |
How isn't
Another alternative might be to just say that function pointers to "safe" #[target_feautre = "sse4.2"] fn bar() -> ();
let a: fn()->() = bar; // FAILS
let b: unsafe fn() -> () = bar; // OK |
I don't quite have the compiler/language chops to evaluate this properly, but if we can get away with it, then this would be great! I have a couple of small comments:
Can you enumerate the different "kinds of unsafe" that Rust has today? As I understand it, they all boil down to UB (which is consistent with the "type of unsafety" this RFC is trying to prevent).
I think it's more galactic then that. I'm pretty sure this qualifies as a new unsafe superpower, since none of the four existing superpowers cover this RFC's use of cc @rust-lang/libs @rust-lang/lang @rust-lang/compiler |
@gnzlbg :
It is, but I don't see how it can simultaneously be true that (in the absence of this RFC) the vendor operations have to be I think of it this way: The proposal to stabilize vendor operations currently says that they'd all be Suppose I want to safely abstract over Intel
On the language level, there are the four you linked to and, with the
I'd argue that the (By precedent of Rust's default 32-bit x86 target enabling SSE2, even though Rust doesn't emit an actual cpuid check at the program entry point, executing the instructions that are permitted by the program's baseline configuration is treated as safe even if there exists a CPU that doesn't support all the instructions in the program's baseline configuration.) |
While correct, that is orthogonal to my point. My point is that the |
Good point. Indeed, the But then, dereferencing a raw pointer could be removed from the list by making it a method on pointer types instead of part of the language syntax. That being the case, the distinction between having to have an item on the list vs. being covered by the item about calling |
I show it below.
The #[inline(always)]
fn generic_add_u8x8(x: u8x8, y: u8x8) -> u8x8 {
#[cfg(target_feature = "sse2")] { unsafe { _mm_add_epi8(x, y) } }
#[cfg(target_feature = "neon")] { unsafe { vaddq_u8(x, y) } }
#[cfg(not(any(target_feature = "sse2", target_feature = "neon")))] {
// fallback implementation, unimplemented!(), ...
compile_error!("generic_add_u8x8 needs sse2 or neon");
}
} That's a zero cost safe
As discussed in the The So maybe when the RFC states that:
it should elaborate a bit more about exactly what is meant here, e.g., by providing an example of such a safe inefficient abstraction. Providing users the tool to create safe efficient abstraction is |
Thanks @hsivonen for opening the RFC! Motivation sectionThe motivation section and the PR description are a bit negative about the During the discussions for that RFC, I have pushed towards that unsafety requirement because I wanted to have assistance by the language itself to ensure that you don't accidentially call a function with a wider Later in the discussion it was revealed that different AVR CPUs apparently map the same instructions to different behaviour, so this gave additional support for the unsafe proposal. This is probably the main contribution to why the RFC ended up with the unsafe requirement inside. Now when arguing for the unsafe requirement, I never wanted it to be the end state. Instead I argued for unsafe so that we can have a second discussion about implementing actual reasoning inside the compiler. If target_feature would have been stabilized without the unsafe requirement, that chance would have been gone :). Before RFC 2045, it was impossible to write A new unsafe superpowerYes, I think the RFC adds a new unsafe superpower. But that is nothing bad! It is much better than shrugging off this potential footgun as something "safe", ignoring that on AVR there is UB involved. If you can implement avoidance of SIGILL inside the compiler, why shouldn't you? Regarding function pointersI think one can allow people to take function pointers of #[target-feature(enable = "avx2")]
fn foo(a: u32) -> (u32, u32) { (a, a) }
fn main() {
let ptr_a: #[target-feature(enable = "avx2")] fn(u32) ->(u32, u32) = foo; // works
let ptr_b: fn(u32) ->(u32, u32) = foo; // ERROR: mismatched types
} The proposed way of using this would be through pointer casting: fn foo_generic(a: u32) -> (u32, u32) { (a, a) }
#[target-feature(enable = "avx2")]
fn foo_avx2(a: u32) -> (u32, u32) { (a, a) }
lazy_static! {
static ref FOO: fn(u32) -> (u32, u32) = {
use std::mem::strip_target_feature;
if runtime_check_that_avx2_available {
let ptr: #[target-feature(enable = "avx2")] fn(u32) -> (u32, u32) = foo_avx2;
unsafe { strip_target_feature(ptr) }
} else {
foo_generic
}
};
}
fn main() {
FOO(42);
} You will still be required to say unsafe somewhere but you can defer it to only the function that determines the function pointer! Why a new function So you have to add a new built in function You could theoretically add such functionality to transmute itself, but this would violate one of the promises of transmute: that it is a trivial byte copy. This gets even more of an issue if the fn pointer is nested deep inside some type you pass to transmute. Also, many people assume that if you can do transmute, you can also do pointer casting. Also,
A library for dynamic checking could expose the functionality in a safe manner through a macro used like: target_feature_function_pointers! {
#[pointers_to(fallback = foo_generic, avx2 = foo_avx2)]
FOO: fn(u32) -> u32,
#[pointers_to(fallback = bar, avx2 = bar_avx2, sse = bar_sse)]
BAR: fn(u32) -> u32,
} Then you can use Another alternative to SummaryThis RFC is an important step in requiring less So it allows you to safely split up your If you fix the function pointers issue e.g. by adopting my proposal from above, you would also enable dynamic dispatch functionality usable from safe code. |
@gnzlbg Unless I'm mistaken, the improvement this RFC proposes is that you should be able to write
instead. Currently with blanket
Good idea, I think that should be added to this RFC. |
Yes, that's it, and I think it is a really nice enhancement; it makes the intrinsics easier and safer to use. Performance-wise, however, there should be no difference. |
The above works as a safe abstraction if With this RFC, I'd expect something like: #[cfg(target = 'x86')]
#[target_feature(enable = "sse2")]
fn generic_add_u8x16(x: u8x16, y: u8x16) -> u8x16 {
_mm_add_epi8(x, y)
}
#[cfg(target = 'arm')]
#[target_feature(enable = "neon")]
fn generic_add_u8x16(x: u8x16, y: u8x16) -> u8x16 {
vaddq_u8(x, y)
} ...which would allow the baseline compilation flags to target 32-bit x86 without SSE2 and ARMv7 without NEON but allow That is, the safe abstraction would abstract over different SIMD ISAs but not over the presence or absence of SIMD support. By efficiency/performance, I mean putting a CPUID check inside the safe abstraction so that instead of the application checking CPUID and taking an (Note: If I don't show up on GitHub for two weeks, it doesn't mean that I've forgotten about this. My apologies in advance.) |
Incorrect, this also works as a safe abstraction if they aren't enabled for the whole program.
Then one can write this abstraction to use
This depends on which fall-back implementation the library author is interested in providing. It could fail to compile, which is safe, panic, which is also safe, or do something else as long as that is safe. This doesn't fail to compile, is safe, portable, and introduces zero run-time cost: #[inline(always)]
fn generic_add_u8x8(x: u8x8, y: u8x8) -> u8x8 {
#[cfg(target_feature = "sse2")] { unsafe { _mm_add_epi8(x, y) } }
#[cfg(target_feature = "neon")] { unsafe { vaddq_u8(x, y) } }
#[cfg(not(any(target_feature = "sse2", target_feature = "neon")))] {
// I could use x + y here, but this is how it works in general:
let mut r = u8x8::splat(0);
for i in 0..8 {
r.replace(x.extract(i) + y.extract(i), i);
}
r
}
} This doesn't fail to compile, it is safe, portable, and does feature detection at compile-time first, and if that fails, dispatches at run-time: #[inline(always)]
fn generic_add_u8x8(x: u8x8, y: u8x8) -> u8x8 {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] {
if cfg_target_feature!("sse2") {
unsafe { _mm_add_epi8(x, y) }
}
}
#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] {
if cfg_target_feature!("neon") {
unsafe { vaddq_u8(x, y) }
}
}
else { x + y }
}
This expectation looks incorrect to me. At least, with this RFC "as is", calling
Incorrect, SIMD libraries do not need to do this. They can if they want to, but for example the
This is partially incorrect. One library could do indeed do this, and on every library call, perform the check (which just fetches a cached value), trusting that the compiler would hoist these checks (run-time feature detection is only executed once per program, sub-sequent calls to the run-time feature detection library just read a cached value). But this is not the only option. The RFC also allows a different library to set some function pointers on initialization and have all its code just blindly call those function pointers, without any checks. I recommend you to either read the RFC and the associated discussion, or just give |
@hsivonen I just want to ping you that we haven't forgotten about this; during the impl period we were just more busy getting |
I feel I need this explained to me like I'm five. I think I'm in particular missing something important about
If SSE2 isn't enabled for the whole program, can the abstraction both be safe and use SSE2 instructions without having the "is SSE2 available" check within
If the program as a whole is compiled for i586 without SSE2 but SSE2 is available at run time, which code path does the above take and how does it know to take it? My goal here is that it would take the first path without a CPUID check at the time of the call while still being safe--the unsafety of the application pledging to the compiler that CPUID has been checked having been at a distance.
Does this do a CPUID dispatch on every call to |
Would it be fair to say that this RFC is moving the point where a target-feature-fn is made unsafe from the fn declaration site to the call site (or reification site, in the case of a function pointer?). If so, I think that makes a lot of sense and I don't immediately see any problems with that setup. One thing that was not crystal clear to me from skimming the RFC and the summary is what the recommended style would be. I would think that the recommended style would be to use target-feature and declare all of your target-feature functions as "safe". Of course, actually calling them from any context that does not itself contain target-feature would be unsafe (because the callee gets to assume their target-features are available, and it's the caller's job to validate that). Then you would have wrappers that do the dynamic testing or whatever and use Is this about right? If so, is there a reason this design was not pursued in the first place? (I've not been following this debate as closely as perhaps I should've been.) |
First, That is, the answer is yes (in general): this function is safe, will use SSE2 at run-time if available (and SSE or something else otherwise), and in most cases you won't have a check per function call because the check will be hoisted in SIMD code. In some cases, however, you will have the check. First, note that to do something depending on the CPU features enabled at run-time, this check needs to happen somewhere in your program at run-time. Second, it is possible to write a The way this works is to do run-time feature detection once (e.g. on binary initialization), and then based on that change globally which function will be called, by either swapping symbols at link-time, or changing the value of a global function pointer. The consequence in both cases is the same: in this approach, the function cannot ever be inlined (because if you inline it then you can't swap symbols/addresses). The There is a third approach: to never have the check, which is not safe. The
The generic path (without SSE2):
You can do this by setting a function pointer on binary initialization (using
The mechanism is the |
IIRC it complicated the implementation (which had other issues until recently), the RFC (where we hadn't agreed over fundamental issues like whether calling an intrinsic should be unsafe or not, whether we needed run-time feature detection or not, etc.), and also it was unclear what to do with trait object methods, function pointers, etc. or whether those problems were even worth solving. Everybody agreed that it would be nicer to only have to write The major problem that pushed this back is that "features" sometimes (e.g. for SIMD) come in hierarchies. That is, when one marks a function with |
Thanks. I was missing the part about the automatic cache and potential hoisting of the check.
The call site of the function that uses more ISA extensions than the caller, yes. (Without more checks or
Yes. |
OK, so, I've been thinking more about this. It still seems to me that this RFC describes the way that I would expect safety to "obviously" work when it comes to target-feature; however, this also feels like something of a "nice to have" sort of refinement. I'd hate to slow down the progress of SIMD just on this point. Ultimately, I wouldn't expect this feature to have a big impact on the "usability" of SIMD, since (a) feature testing will probably be encapsulated in some library that would handle the transition from safe-to-unsafe So the main case being addressed here is accidentally invoking the wrong target-feature fn from your fn, right (as well as encapsulating other uses of unsafe)? This is certainly important, but not make or break. So probably what I'm saying is that I'd like to hear a bit more from those folks who've been driving the SIMD design to completion about where this fits in the "priority of things". Maybe this is better treated as an idea to 'file away for later'? (Relatedly, are there concerns about this RFC "on the merits" -- that is, that do not have to do with prioritization?) |
Also, @hsivonen, is your intention that the setting of |
The main case being addressed is not having a situation where either stuff becomes unsafe wholesale (the whole part of the program that uses non-baseline instruction set extensions) or there's cheating about what's safe or there are non-programmer-controlled run-time checks.
Yes, mostly because SSE2 should work without ceremony with the default i686 target and the hoop jumping should be up to the users of the i586 target. |
@rfcbot fcp postpone It seems like discussion has stalled here. Although I am sympathetic to the aims of this RFC -- as I have previously expressed -- I also have the feeling that the time may not be ripe here. This feels like it's a kind of "side concern" to the main thrust of getting SIMD up and going in some form on stable; it also seems like something that would be better addressed once that work is more mature. Therefore, I move to postpone, and propose that we re-address this once some core part of SIMD is stable (or at least much closer). |
Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged teams:
No concerns currently listed. Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
I disagree with postponement. For me, safety is the core distinguisher of Rust from languages like Go, C or C++ (Go is not threadsafe!), and being able to write fast code safely is important. I'm saying this as someone who argued for making
Merging the RFC doesn't mean that it has to slow down SIMD stabilisation. In fact, I believe that SIMD is highly useful without this feature as well. |
@est31 I think the main problem is that without a prototype implementation of this RFC it is hard to get a feeling for it. Iterating on a prototype upstream behind a feature gate, and then revisiting this RFC once things are clearer sounds like a more productive path to me. Somebody just needs to pick the torch and do it. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period is now complete. |
Postponed as per completed FCP. Thanks @hsivonen for the RFC! |
RFC 2045 defines
[#target_feature]
as applying only to unsafe functions. This RFC allows[#target_feature]
to apply safe functions outside trait implementations but makes it unsafe to call a function that has instruction set extensions enabled that the caller doesn't have enabled (even if the callee isn't markedunsafe
). Taking a function pointer to a safe function that has[#target_feature]
is prohibited.[#target_feature]
applying only to functions that are declaredunsafe
makes Rust's safe/unsafe
distinction less useful, because it causes unnecessarily many things to becomeunsafe
. Specifically, it causes operations that depend on instruction set extensions, such as SIMD operations to becomeunsafe
wholesale and it causes the entire part of the program that uses instruction set extensions (compared to the program-wide baseline) to becomeunsafe
.Worse, the logic that causes instruction set extension-using operation like SIMD operations to become
unsafe
(they might execute UB unless the programmer has properly checked at run time that the instruction set extension is supported but the host CPU) means that it's impossible to create efficient safe abstraction over theunsafe
operations without cheating about the notion of safety.Rendered