Skip to content

Use const generics for stdarch intrinsics #83167

Closed
@Amanieu

Description

@Amanieu

Background

rust-lang/stdarch#248

core::arch contains architecture-specific vendor intrinsics, some of which have arguments that are required to be compile-time constants. This is tricky because LLVM will often assert/ICE if a constant is not passed into the LLVM intrinsic at the LLVM IR level. This was previously handled by using a match statement to manually monomorphize the intrinsic:

#[inline]
#[target_feature(enable = "sse")]
#[cfg_attr(test, assert_instr(shufps, mask = 3))]
#[rustc_args_required_const(2)]
#[stable(feature = "simd_x86", since = "1.27.0")]
pub unsafe fn _mm_shuffle_ps(a: __m128, b: __m128, mask: i32) -> __m128 {
    let mask = (mask & 0xFF) as u8;

    macro_rules! shuffle_done {
        ($x01:expr, $x23:expr, $x45:expr, $x67:expr) => {
            simd_shuffle4(a, b, [$x01, $x23, $x45, $x67])
        };
    }
    macro_rules! shuffle_x67 {
        ($x01:expr, $x23:expr, $x45:expr) => {
            match (mask >> 6) & 0b11 {
                0b00 => shuffle_done!($x01, $x23, $x45, 4),
                0b01 => shuffle_done!($x01, $x23, $x45, 5),
                0b10 => shuffle_done!($x01, $x23, $x45, 6),
                _ => shuffle_done!($x01, $x23, $x45, 7),
            }
        };
    }
    macro_rules! shuffle_x45 {
        ($x01:expr, $x23:expr) => {
            match (mask >> 4) & 0b11 {
                0b00 => shuffle_x67!($x01, $x23, 4),
                0b01 => shuffle_x67!($x01, $x23, 5),
                0b10 => shuffle_x67!($x01, $x23, 6),
                _ => shuffle_x67!($x01, $x23, 7),
            }
        };
    }
    macro_rules! shuffle_x23 {
        ($x01:expr) => {
            match (mask >> 2) & 0b11 {
                0b00 => shuffle_x45!($x01, 0),
                0b01 => shuffle_x45!($x01, 1),
                0b10 => shuffle_x45!($x01, 2),
                _ => shuffle_x45!($x01, 3),
            }
        };
    }
    match mask & 0b11 {
        0b00 => shuffle_x23!(0),
        0b01 => shuffle_x23!(1),
        0b10 => shuffle_x23!(2),
        _ => shuffle_x23!(3),
    }
}

The downside of this approach is that it generates huge MIR and severely bloats the size of libcore to the point where it actually affects compilation time.

The #[rustc_args_required_const] attribute was used to restrict arguments to constants so that we could eventually replace this with const generics. Note that it effectively only acts as a hard lint, the match is still needed to manually monomorphize the LLVM intrinsic.

New const generics support

rust-lang/stdarch#1022

All the intrinsics in stdarch have been converted to use the newly stabilized const generics:

#[inline]
#[target_feature(enable = "sse")]
#[cfg_attr(test, assert_instr(shufps, mask = 3))]
#[rustc_legacy_const_generics(2)]
#[stable(feature = "simd_x86", since = "1.27.0")]
pub unsafe fn _mm_shuffle_ps<const mask: i32>(a: __m128, b: __m128) -> __m128 {
    static_assert!(mask: i32 where mask >= 0 && mask <= 255);
    simd_shuffle4(
        a,
        b,
        [
            mask as u32 & 0b11,
            (mask as u32 >> 2) & 0b11,
            ((mask as u32 >> 4) & 0b11) + 4,
            ((mask as u32 >> 6) & 0b11) + 4,
        ],
    )
}

To preserve backwards compatibility with the already stabilized intrinsics using #[rustc_args_required_const], a new attribute #[rustc_legacy_const_generics] was added in #82447 which rewrites function calls of the form func(a, b, c) to func::<{b}>(a, c).

This new attribute is not intended to ever be stabilized, it is only intended for use in stdarch as a replacement for #[rustc_args_required_const].

#[rustc_legacy_const_generics(1)]
pub fn foo<const Y: usize>(x: usize, z: usize) -> [usize; 3] {
    [x, Y, z]
}

fn main() {
    assert_eq!(foo(0 + 0, 1 + 1, 2 + 2), [0, 2, 4]);
    assert_eq!(foo::<{1 + 1}>(0 + 0, 2 + 2), [0, 2, 4]);
}

Open questions/issues

  • #[rustc_args_required_const] allowed constant expressions derived from generic arguments in the caller, but this is not allowed with const generics. This technically makes this a breaking change.
  • Another potential breaking change is that the intrinsics now use a post-monomorphization static_assert to validate constant arguments. Previously, intrinsics would either use runtime asserts or simply pick an arbitrary default behavior for out-of-range constants.
  • Do we want to force all callers to use the const generics syntax in the 2021 edition?
    • Pros:
      • It makes it clearer that these intrinsics expect a constant as an argument.
      • Intrinsics are rendered with the const generics syntax in rustdoc.
    • Cons:
      • We will still need to support the #[rustc_legacy_const_generics] argument rewriting anyways for 2015/2018 code.
      • Intrinsics no longer have the same function signature as the original C vendor intrinsics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-langRelevant to the language team, which will review and decide on the PR/issue.T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.T-rustdocRelevant to the rustdoc team, which will review and decide on the PR/issue.relnotesMarks issues that should be documented in the release notes of the next release.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions