rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.45k stars 12.6k forks source link

`const` function arguments for intrinstrics/simd #47980

Closed gnzlbg closed 6 years ago

gnzlbg commented 6 years ago

Thist issue lifts https://github.com/rust-lang-nursery/stdsimd/issues/248 into rust-lang/rust

Problem

Many intrinsics, in particular, many SIMD intrinsics, work only on immediate-mode registers. We can currently implement them in the compiler directly, handling constant function arguments in the implementation.

This doesn't allow users to easily build abstractions on top of these intrinsics because they can't do what the compiler can do. Abstractions can, however, be built (I show what the stdsimd crate does below).

Currently, for many intrinsics, we don't do this in the compiler, but in libraries, like the stdsimd crate (check out, for example, _mm_i32gather_epi32).

In the stdsimd crate, new contributors typically clash against our workarounds for dealing with these intrinsics pretty early on, often in their first pull-request (e.g. see https://github.com/rust-lang-nursery/stdsimd/pull/311).

The API of these intrinsics in the stdsimd crate uses run-time values

fn foo(x: u8, imm8: u8) { ... }

and then it uses constify_xxx! macros like constify_imm8! to map these run-time values into compile-time constants:

fn foo(x: u8, imm8: u8) { 
    macro_rules! call {
      ($imm8:expr) => (foo_intrinsic(x, $imm8))
    }
    constify_imm8!(imm8, call)
}

These constify macros just match against every possible run-time value, and call a function with a constant:

macro_rules! constify_imm8 {
    ($imm8:expr, $expand:ident) => {
        #[allow(overflowing_literals)]
        match ($imm8) & 0b1111_1111 {
            0 => $expand!(0),
            1 => $expand!(1),
            2 => $expand!(2),
            ...
           254 => $expand!(254),
            _ => $expand!(255),
        }
    }
}

The status quo does not scale, neither for those wanting to write higher-level wrappers over the stdsimd crate, nor for some of the intrinsics that require huge match arms (e.g. bextri is currently disabled because constify_imm16! makes the compile-times of the stdsimd crate explode from 1-3 minutes to ~30 minutes).

Ideal solution

I would like to be able to specify that a function argument must be const:

/// This function takes a run-time argument `x` 
/// and a constant argument `y`.
fn foo(x: u8, const y: u8) { ... }

This is what the Intel spec tries to specify when it uses const int. It fails because in C const does not mean the same thing as in Rust, but this makes it an opportunity: Rust could follow the C spec better than C does.

Alternatives

We could just implement these intrinsics in rustc. There are many many of them, but this can be done. Rustc actually already does this for many intrinsics, and errors of arguments not being constants are already reported.

The main issues I see with this is that

Moving these intrinsics into the compiler would allow us to stabilize simd without having to wait for const function arguments or equivalent language features.

hanna-kruppe commented 6 years ago

This "ideal solution" seems like it's strictly weaker than, and subsumed by, const generics.

gnzlbg commented 6 years ago

@rkruppe I agree, but I think that semantics do matter: const-generics is a feature for using "type-level values", while the users of these intrinsics intent is to "pass const function arguments". If the C spec states:

void bar(char, const char, char, const char);

I think it is a major ergonomic improvement for users to be able to map that to:

fn bar(u8, const u8, u8, const u8);

instead of directly using const-generics:

fn bar<const u8, const u8>(u8, u8, u8);

Obviously, I expect both features to be able to do the same thing, and I expect const-function arguments to just desugar to const-generics internally, in the same way that impl Trait in function argument position just desugars into generics.

Does that make sense?


Or in other words, yes I agree with you, this would just be "nicer" syntax for some use-cases of const-generics.

hanna-kruppe commented 6 years ago

Mapping

void foo(const u8);

to

fn foo(const _: u8);

would be wrong. It means something very different (as you said), even though some vendors have abused this notation in C. If people are really going to use const arguments in Rust functions to mirror const arguments of C functions, that would be a point against adding this syntax.


If it's truly just syntactic sugar for a specific kind of const generics, then it certainly doesn't block stabilization of intrinsics (or anything else, for that matter), so I don't understand most of the inital post in that light.

Also, I'd rather gather experience with plain old const generics before adding sugar for specific use cases (just as we've had a lot of experience with type generics before adding impl Trait in argument position).

gnzlbg commented 6 years ago

If it's truly just syntactic sugar for a specific kind of const generics, then it certainly doesn't block stabilization of intrinsics (or anything else, for that matter), so I don't understand most of the inital post in that light.

Well, we don't have const-generics yet, so until then, those intrinsics are blocked. What we could do is expose the intrinsics under a different name with a different API (and eventually different trade-offs) that we can deprecate later on, when something like this becomes available.

Also, IIUC the const function arguments is more limited in scope than the const-generics RFC because the const function arguments don't necessarily need to be usable in type-argument position. This could probably allow the machinery behind const/associated const to implement this, which means we could get this sooner (maybe for the initial SIMD stabilization round).


Mapping ... would be wrong.

It would be wrong in C. But the specs are not C, the specs are in a language that looks like C but where const char as the type of a function argument requires the argument to be a compile-time constant (and that's how they are implemented in C compilers, using compiler intrinsics to achieve this effect). Unless I am misunderstanding something, that is exactly what const _: u8 would mean in Rust.

When it comes to real C, then yes, const in C does not mean the same as const in Rust, but this difference already exists in Rust (this proposal does not introduce it).

hanna-kruppe commented 6 years ago

This could probably allow the machinery behind const/associated const to implement this, which means we could get this sooner (maybe for the initial SIMD stabilization round).

You're right that it's possible to implement this feature without implementing everything required for const generics. However, if you're proposing that, this feature can't be justified as being just sugar for const generics, since it predates const generics and may turn out to be incompatible with the exact form of const generics eventually adopted. In particular, const generics may wind up having some restrictions on what sorts of values can be used as const arguments -- are we sure this feature will have the same restrictions (and do we even want it to?).

But I am quite opposed to adding an ad hoc feature just to be able to stabilize some feature a bit (possibly only in the order of months) sooner. If we add this feature, I think it needs to be justified independently and stand on its own.

It would be wrong in C.

I may have misunderstood you. The user base of C is much larger, and the semantics of C are much more well known, therefore I could understand an argument based on analogy to C (even though I'd disagree for reasons stated). If you're only talking in analogy to that vendor documentation, I don't understand this statement (emphasis mine):

[...] I think it is a major ergonomic improvement for users to be able to map that to: [...]

Translating those documents to Rust signatures is a one-time job for expert users, and syntactic differences are the least of the problems involved. I don't think making it a tiny bit easier to transliterate those documents to Rust is even remotely enough justification to add any language feature. 99.99% of Rust users (including those that use the intrinsics) would not benefit at all from it.

gnzlbg commented 6 years ago

are we sure this feature will have the same restrictions (and do we even want it to?).

We can be conservative.

If we add this feature, I think it needs to be justified independently and stand on its own.

Fair enough.

Translating those documents to Rust signatures is a one-time job for expert users,

Every abstraction over SIMD until the end user must do this, and the end user must not pass these abstractions run-time values.

hanna-kruppe commented 6 years ago

We can be conservative.

Sure, but we need to be absolutely certain we're conservative enough to be forward compatible with const generics. And the more conservative we are, the less general and appealing this feature is as a feature of its own.

Every abstraction over SIMD until the end user must do this, and the end user must not pass these abstractions run-time values.

Good point, but those people can (and probably should) reference the signatures in stdsimd (edit: or whatever layer of abstraction they're building on). Doing that saves them the entire effort of transliterating the signature (edit: and any effort for mapping to a higher level of abstraction, if they build on one), while matching vendor documentation only makes the transliteration minimally simpler.

Not to mention that I don't think it's an undue burden to convert "const arguments" to const generics while transliterating the syntax. It's just one more minor syntactic difference.

gnzlbg commented 6 years ago

those people can (and probably should) reference the signatures in stdsimd (edit: or whatever layer of abstraction they're building on).

What do you mean?

hanna-kruppe commented 6 years ago

Someone who writes an abstraction over stdsimd should (and hopefully will) look at the stdsimd signatures to help write their own library's signatures, rather than going all the way back to the vendor documentation. Likewise, someone who writes an abstraction over a higher level SIMD library will look at that library's function signatures, not at any of the layers below. So similarity to the vendor documentation is at most relevant for the lowest level of the stack, the people creating stdsimd.

gnzlbg commented 6 years ago

Someone who writes an abstraction over stdsimd should (and hopefully will) look at the stdsimd signatures to help write their own library's signatures, rather than going all the way back to the vendor documentation.

The problem is that stdsimd signatures say that some intrinsics take run-time values when they actually require compile-time constants. stdsimd can make this a compile-time error with some compiler magic, but libraries abstracting over stdsimd cannot, and they have no way of expressing "function arguments that are compile-time constants", so they would either need to transform run-time arguments into compile-time ones (which the corresponding hit to compile-times), or they will need to use "imagination" to allow their users to pass compile-time contants ergonomically (associated consts can be used with traits and generic arguments to emulate passing compile-time constants as function arguments).

EDIT: For example, something like this in the wrapper over stdsimd:

pub trait U16Constant {
  const X: u8;
}

pub fn barbaz<T: U16Constant>(x: u16) {
   coresimd::barbaz(x, T::X)
}

and like this in user code of that library:

use supersimd::{U16Constant, barbaz};

struct Leet;
impl U16Constant for Leet { const X: u8 = 1337; }

barbaz::<Leet>(4);

It can be done, but, I think its suboptimal. That's all.

hanna-kruppe commented 6 years ago

I am well aware and agree this is a problem that needs solving. I was talking solely about the "it looks closer to vendor documentation which makes it easier to write the equivalent Rust signature" angle and arguing that only the stdsimd authors, and nobody else, needs to reverse engineer a rust signature out of vendor docs or C headers.

scottmcm commented 6 years ago

As this is proposing a new language feature, I believe it will need an RFC, and thus should live over on https://github.com/rust-lang/rfcs

gnzlbg commented 6 years ago

I'll post a pre-RFC in internals when I have the time.