Skip to content
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

Improve handling of immediate mode arguments #248

Closed
gnzlbg opened this issue Dec 22, 2017 · 42 comments
Closed

Improve handling of immediate mode arguments #248

gnzlbg opened this issue Dec 22, 2017 · 42 comments

Comments

@gnzlbg
Copy link
Contributor

gnzlbg commented Dec 22, 2017

Many intrinsics take immediate more argument (that is, some arguments must be compile-time constants).

We currently implement them by taking these arguments as run-time values, and "converting" them to compile-time values using a match statement with a match arm for each particular case:

fn foo(imm8: u8) {
    match imm8 {
        0 => foo_(0),
        1 => foo_(1), 
        ... 256 match arms later ...
        u8::MAX => foo_(u8::MAX),
    }
}

If a user pass them a compile-time value the optimizer can select the appropriate match arm and remove the rest of the code.

Passing them a run-time value should generate an error, but with this approach the compiler silently accepts this code, and generates code for all match arms, potentially making binary sizes / compile-times explode.

Even when the user passes a compile-time constant to the immediate arguments, some intrinsics (AMD loves these) require match statements with >= 65k arms that destroy compile-times in all cases (from 2x to 3x increase in compile-times of the whole stdsimd test suite if one of this functions is used somewhere). The following intrinsics have such extremely large match arms:

  • sse4a::_mm_extracti_si64
  • sse4a::_mm_inserti_si64
  • tbm::bextri

This issue blocks completing SSE4a in #40 and fixing #26 . It was previously discussed in #190 but never got an issue of its own.

Amongst the proposed solutions were to either implement these in the compilers or wait till const generics + extensions arrive (e.g. foo(x: const i8) to require x to be a compile-time constant).

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Feb 3, 2018

@alexcrichton I have lifted this issue to the rust-lang/rust repo.

I think that handling immediate mode arguments properly is something we should attempt to fix before stabilization somehow.

That is, either we get foo(const imm8: T), or we should enumerate the intrinsics with immediate mode arguments and special-case them in trans somehow.

@alexcrichton
Copy link
Member

Thanks! I'm not sure I 100% agree we should block stabilization, but we can always sort out these details later :)

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Feb 4, 2018

I guess my thoughts are more aligned with either:

  • don't stabilizing these intrinsics at first (some of them are very useful, so I'd rather not do this)
  • use the compiler to enforce that the arguments passed to these are compile-time constants somehow (not necessarily a language feature, but either an unstable attribute or just implementing these as compiler-intrinsics and exposing them via stdsimd).

Both choices probably will mean that the strongly-typed SIMD wrapper over stdsimd won't be able to offer the same guarantees that stdsimd does (e.g. the wrapper will need to use the constify! macros internally because otherwise it can't map run-time arguments to compile-time constants).

The idea of doing this in the compiler is what @eddyb basically mentioned in #190 .

@alexcrichton
Copy link
Member

Oh right yeah I should mention that if we had a mechanism I'd prefer to stabilize these with "requires a constant semantics" but I'm under the impression that such a mechanism would be pretty far off, and in the meantime our local workaround isn't too bad.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Feb 5, 2018

Oh I thought that we had a mechanism for this in the compiler because the prefetch intrinsic (and others) require this, but it looks like the core::intrinsics module works around this by adding an intrinsic for every combination of constant arguments.... :D

  • prefetch_read_data
  • prefetch_read_instruction
  • prefetch_write_data
  • prefetch_write_instruction

(on clang its just __builtin_prefect(....) and clang tests that the arguments are constants or emits a "nice" monomorphization time error)

I don't think adding core::intrinsics::bextri_imm8_0...core::intrinsics::bextri_imm8_16384 is an option :D

@eddyb ?

@eddyb
Copy link
Member

eddyb commented Feb 5, 2018

We do have the mechanism but it's specifically only used for shuffles right now.

Checking/promotion-to-guaranteed-constant: https://github.com/rust-lang/rust/blob/07ea2604075d6f896addce0e6949c7cf25dd3715/src/librustc_mir/transform/qualify_consts.rs#L760-L768 (oh come on GitHub... no pretty code?)

Custom lowering of MIR call arguments (this is only needed because arrays are not immediates - for most of the usecases you care about, special-casing this is unnecessary): https://github.com/rust-lang/rust/blob/07ea2604075d6f896addce0e6949c7cf25dd3715/src/librustc_trans/mir/block.rs#L521-L539

So it'd only be a few more lines to add more intrinsics to the checking/promotion side.
We could even put the information in librustc_platform_intrinsics and read it from there.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Feb 5, 2018

@eddyb do you think this could be implemented as a stdlib-internal function attribute (e.g. that specifies the argument name) or as a function argument attribute (these are not a thing, right?) ?

That way we could keep doing things in the library without having to mess with librustc_platform_intrinsics (adding stuff here is not hard, but it raises the barrier of contributing to stdsimd quite a bit, and right now, we need all the help we can get)

@eddyb
Copy link
Member

eddyb commented Feb 5, 2018

@gnzlbg The problem as I see it is that librustc_platform_intrinsics is wrong if it allows you to call one of the intrinsics without a constant value. But sure, we could have an attribute on the intrinsic itself, holding argument indices or something.
Attributes prefixed with rustc_ are perma-unstable and we can easily use one of those.

@alexcrichton
Copy link
Member

@eddyb do you know how easy it would be to experiment with an attribute like this? (aka how hard it would be to implement it)

I'm sort of also worried about things like:

let f: fn(..) = stdsimd::vendor::function_that_requires_constant;

In a situation like that, we presumably want to error, right? Are there other locations in the compiler that will be affected by such "this argument must be a constant" logic?

@eddyb
Copy link
Member

eddyb commented Feb 5, 2018

@alexcrichton Depends if you want to apply on just the intrinsic or also on some wrapper function and have that wrapper function monomorphized. The latter is harder and basically const generics.

@alexcrichton
Copy link
Member

@eddyb in our case it's all wrapper functions (which transitively call the intrinsic). But ok thanks for the info!

@eddyb
Copy link
Member

eddyb commented Feb 5, 2018

Oh, sorry, I was getting confused between the intrinsics and the wrappers. There isn't much to do there before const generics lands and even then this const arg: T feature would need an RFC.

@alexcrichton
Copy link
Member

I've opened up rust-lang/rust#48018 with @eddyb's strategy from above which adds a new unstable attribute which can guarantee that arguments to a call are constant. That I think is the "biggest hole" today but there's still two pieces remaining:

  • The attribute doesn't disallow acquiring function pointers, so those can still be taken and passed.
  • We can't actually take advantage of it yet in the definition of the various intrinsics. We'll need more compiler support eventually for removing the constify macros, and for now we'll have to leave them all as-is.

@eddyb
Copy link
Member

eddyb commented Feb 5, 2018

Aside from function pointers, there's also the Fn* traits that shouldn't be implemented.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Feb 5, 2018

In the mean time this might be a solution that we could apply after stabilization: https://internals.rust-lang.org/t/pre-rfc-const-function-arguments/6709/22

@newpavlov
Copy link
Contributor

In aes intrinsics (and probably some others as well) i32 is used as type for imm8 argument. If I understand correctly it's done to follow C signature for related intrinsics. But I think we better just use u8 there, especially if we'll add language-level support for const argument . Opinions?

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Feb 12, 2018

@newpavlov We map C's int to i32 as a rule. The type that int is mapped to is orthogonal to how we handle const arguments, and should probably be discussed elsewhere.

@tmiasko
Copy link
Contributor

tmiasko commented Dec 21, 2020

On x86-64, stdarch contributes now 56 MB out of 76 MB of libcore.rlib size. It is also responsible for more than doubling the size of libcore.rlib since rustc 1.47 rust-lang/rust#80245. Most of this can be directly attributed to the current handling of immediate arguments combined with support for new intrinsics (for example moving functions calls out of macros reduces rlib size by 9.8 MB in #973).

@Amanieu
Copy link
Member

Amanieu commented Dec 21, 2020

The proper solution would be to turn these intrinsics into const generics (with some syntax sugar) so that each variant is instantiated on demand.

@alex
Copy link
Member

alex commented Jan 1, 2021

It's probably of note that min_const_generics are now stable on Rust nightly -- that wasn't true when this issue was first filed, so perhaps that's the answer.

@newpavlov
Copy link
Contributor

@alex
Note that changing immediate mode arguments to const generics would be a breaking change (i.e. changing fn foo(n: i32) to fn foo<const N: i32>()), since we already have stabilized intrinsics with such arguments, so it's a non-starter. Even without backward compatibility concerns, I think that using "template" parameters for immediate mode arguments will be extremely weird and I would strongly prefer to have "const fn arguments" as proposed by @gnzlbg.

@oli-obk
Copy link
Contributor

oli-obk commented Jan 4, 2021

Even without backward compatibility concerns

Note that you are using a feature that was never meant for the way it is being used right now. It was literally just meant for being used on the simd_shuffle intrinsic argument. Using it on functions works by accident and afaict was never RFCed or discussed with the lang team or const eval team.

since we already have stabilized intrinsics with such arguments

Which I am very unhappy about, but I guess that ship has sailed. I still think we should deprecate them and move to const generics, which exist exactly for the use case of having a function that is generic over a constant value, but that's just my personal preference. I have no idea at what point these promoted function arguments will break down on us, but their implementation is totally wonky and was not meant for anything but plain integer literals.

@oli-obk
Copy link
Contributor

oli-obk commented Jan 5, 2021

I don't think we should do a breaking change. We can keep them as they are, but I think we should have new functions using const generics (which are fully specified and soon stable) and mark the other ones as deprecated. My problem is with the promotion that happens without any syntactical obviousness, is completely underspecified (does it allow generics, does it allow const fns, ...) and not actually RFCed by any team. In addition, the way it is specified right now, there is no way to know inside the function that the argument is a constant, which circumvents analyses like const_evaluatable_checked.

If you want this in argument position, we can do something like

fn foo<const N: u32>(x: i32, y: ConstU32<N>) {}

which is then called as

foo(42, ConstU32::<99>)

A macro could make it less verbose to construct such values, or we could consider adding a way to allow user-defined construction from literals, at which point foo(42, 99) could become a reality again.

@newpavlov
Copy link
Contributor

newpavlov commented Jan 5, 2021

@oli-obk
Why introduce such roundabout functionality instead of proper const arguments? Especially considering that for it to be convenient you have to rely on user-defined construction from literals, which AFAIK does not even have a drafted proposal (in the form which would allow foo(42, 99)). Const arguments look like a simpler solution implementation-wise (compiler can interpret them as a simple sugar for classic generics), less confusing for users, easier to teach, and require no deprecation of already stable functions.

Yes, some may not like that const arguments could be used for writing methods like fn zeros(const N: usize, const M: usize) -> Matrix<N, M>, but to me it looks like a matter of taste and personally I find such hypothetical API quite convenient. IIRC @RalfJung has even proposed to go even further and allow "type arguments" (i.e. allow methods like fn zeros(type T: Float, const N: usize, const M: usize) -> Matrix<T, N, M>), but it's another discussion for another time.

@oli-obk
Copy link
Contributor

oli-obk commented Jan 5, 2021

Why introduce such roundabout functionality instead of proper const arguments?

because it is entirely unclear whether the accidentally stabilized const arguments in this crate will ever have a stable counterpart, and I don't find "we accidentally stabilized an underspecified and broken version of it" a motivating enough reason to stabilize it. Sure we can keep this until there's a decision on the proposal, but in the years that we'll work on getting that to stable, we should limit the arbitrary usage of these APIs.

I am very unhappy about this extensive usage of a rustc_ attribute which has no accepted plan forward. I think we should keep the existing attributes, even if removing them is not a breaking change, but we should not stabilize any other functions with that attribute without an accepted RFC that allows us to remove said attribute.

EDIT: Just to be clear: I am annoyed at myself (and not anyone else) for not realizing that there was a plan to stabilize stuff in stdarch that uses rustc_args_required_const when all of that was discussed. We should probably make an MCP to the lang team so that a concious decision can be made about "some day in the future we will support something like the "const fn arguments" pre-RFC (and until the lang team has made a decision on that, let's have a moratorium on stabilizing any more of these functions, and I promise I'll stop talking about deprecating them until (if ever, hopefully not) a decision is made to forbid them). Then we can vet the attribute and make sure its implementation satsifies the constraints we have and isn't a fully powered const context with features beyond what we currently allow even in inline const expressions.

@RalfJung
Copy link
Member

RalfJung commented Jan 6, 2021

IIRC @RalfJung has even proposed to go even further and allow "type arguments" (i.e. allow methods like fn zeros(type T: Float, const N: usize, const M: usize) -> Matrix<T, N, M>), but it's another discussion for another time.

I don't recall proposing this... though I do agree that this seems like the "logical next thing", then -- but it goes directly against the Rust design decision to clearly separate static and dynamic arguments.

@oli-obk

I have no idea at what point these promoted function arguments will break down on us, but their implementation is totally wonky and was not meant for anything but plain integer literals.

We could reject anything but integer literals and named constants -- but I assume that would be a massive breaking change.

Then we can vet the attribute and make sure its implementation satsifies the constraints we have and isn't a fully powered const context with features beyond what we currently allow even in inline const expressions.

How hard would it be to compile them to inline const expressions? At least then we can avoid relying on promotion. It would expand the scope of what is accepted in these arguments to "all const-expressions", but that might already be pretty much what explicit promotion checks?

@bjorn3
Copy link
Member

bjorn3 commented Jan 6, 2021

We could reject anything but integer literals and named constants -- but I assume that would be a massive breaking change.

I don't think anything else is used much or even at all.

@RalfJung
Copy link
Member

RalfJung commented Jan 6, 2021

Okay, let's see what crater says: rust-lang/rust#80759

@varkor
Copy link
Member

varkor commented Jan 7, 2021

since we already have stabilized intrinsics with such arguments

Which I am very unhappy about, but I guess that ship has sailed. I still think we should deprecate them and move to const generics, which exist exactly for the use case of having a function that is generic over a constant value, but that's just my personal preference.

The fact that these intrinsics were stabilised seems like an abuse of the RFC process, as they require a new language feature that was never properly discussed in public, nor explicitly approved. I strongly agree that const arguments should be deprecated, as they're a hack that was a stopgap before const generics were ready for use. Whether one prefers the syntax or not is irrelevant: new language features should be approved via the RFC process.

It seems like it would be very easy to define macros that act like const argument functions, modulo the !, implemented using const generics. What motivation is there to support this in the language natively?

EDIT: Just to be clear: I am annoyed at myself (and not anyone else) for not realizing that there was a plan to stabilize stuff in stdarch that uses rustc_args_required_const when all of that was discussed.

I do think something has gone wrong with the process. This should have been a much more visible change to people not keeping up with the intrinsics work.

@Amanieu
Copy link
Member

Amanieu commented Jan 7, 2021

I'm happy to block all new uses of rustc_args_required_const and convert all unstable functions in stdarch that currently use it to use const generics instead.

The question is, what should we do about the functions which are already stable? I can think of two possibilities:

  • Leave them as they are. This is somewhat suboptimal since these functions contain massive match statements that bloat the size of libcore. This has been shown to cause performance regressions in compilation time.
  • We could convert them to const generics, but mark them with an attribute to support calling them with the old function-like syntax just for backwards compatibility reasons. Essentially the compiler would magically rewrite _mm_extract_ps(foo, CONST) to _mm_extract_ps<CONST>(foo). The old-style call syntax would be marked as deprecated and would be forbidden in 2021 edition code.

@varkor
Copy link
Member

varkor commented Jan 9, 2021

The question is, what should we do about the functions which are already stable? I can think of two possibilities:

I agree the former leaves much to be desired. Perhaps something to the effect of the second option could be added to the 2021 edition planning document.

We could convert them to const generics, but mark them with an attribute to support calling them with the old function-like syntax just for backwards compatibility reasons.

Presumably #[rustc_args_required_const] could just be changed to have this new behaviour.

@RalfJung
Copy link
Member

RalfJung commented Jan 9, 2021

Following the discussion in rust-lang/rust#76001, one possible option (maybe just an intermediate solution) would be to turn rustc_args_required_const arguments into inline consts. At least that would decouple them from the somewhat fragile mechanism of promotion, and instead use the much more well-defined mechanism of inline consts. (It would also allow us to clean up promotion, reducing technical debt.) This should be backwards-compatible, so it could also be done on old editions.

@Amanieu
Copy link
Member

Amanieu commented Jan 9, 2021

My concern with inline consts is that they don't support referring to generic parameters of the containing fn/impl. As far as I know that is only real (user-visible) difference between inline consts and explicit promotion.

@RalfJung
Copy link
Member

RalfJung commented Jan 9, 2021

They don't support this yet, but support is planned (though @lcnr also has some plans about const-eval-well-formed things here that might make this less ergonomic than explicit promotion).

@tmiasko
Copy link
Contributor

tmiasko commented Mar 6, 2021

The size of libcore_arch-*.rlib, build with cargo build --release on x86_64-unknown-linux-gnu, shrunk from 58MB just before initial conversion to const generics, to 40MB as of 35d17a6.

The largest remaining wrappers are for avx512f intrinsics. Using MIR dump as a proxy for size in rlib, their size often exceeds 1MB, while most have merely a few kB.

@lqd
Copy link
Member

lqd commented Mar 6, 2021

I'll focus on avx512f.rs today, some of the constify macros it uses are huge.

@newpavlov
Copy link
Contributor

newpavlov commented Mar 6, 2021

@oli-obk

We should probably make an MCP to the lang team so that a concious decision can be made about "some day in the future we will support something like the "const fn arguments" pre-RFC (and until the lang team has made a decision on that, let's have a moratorium on stabilizing any more of these functions, and I promise I'll stop talking about deprecating them until (if ever, hopefully not) a decision is made to forbid them).

Has a decision been made by the lang team? I believe that stabilization of "template"-based intrinsics should be blocked on this decision, otherwise we may end up with two incompatible ways of doing the same thing.

Also I want to note that AFAIK Rust has explicitly choose to follow vendor intrinsic APIs as close as possible, even though in some cases it makes sense to deviate from them (e.g. by using unsigned types instead of signed ones). Using "template" arguments for immediate mode arguments goes against this choice and I believe will only result in confusion down the line.

@oli-obk
Copy link
Contributor

oli-obk commented Mar 6, 2021

Have decision been made by the lang team? I believe that stabilization of "template"-based intrinsics should be blocked on this decision, otherwise we may end up with two incompatible ways of doing the same thing.

I don't think so, but it's also not really a problem since we are already using the non-generic version. Using a generic version is entirely in the space of the libs team as far as I can tell. So when a PR to bump stdarch in the rustc repo is opened, we can require a libs team FCP on it.

@RalfJung
Copy link
Member

Now that #1022 is resolved, what is still being tracked here?

@Amanieu
Copy link
Member

Amanieu commented May 15, 2021

This is done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests