Runtime: Exploration of API Look and Feel for x86 SSE Platform Intrinsics

Created on 2 Aug 2017  Â·  18Comments  Â·  Source: dotnet/runtime

NOTE: This is currently more of an exploration for API look and feel than an actual proposal.

Recently, the design document for .NET Platform Dependent Intrinsics was published (https://github.com/dotnet/designs/blob/master/accepted/platform-intrinsics.md).

One of the things that still needs to be determined is the exact "look and feel" of the exposed APIs. Given the requirements put forth in the design doc, I explored what exposing the x86 SSE intrinsics might look like.

I believe we need a minimum of 3 attributes:

  • Intrinsic identifies a method as exposing an intrinsic

    • It might be worthwhile letting the constructor take a set of strings that indicates alternative aliases for the function

  • Literal identifiers a parameter that is required to be a literal (there are a few instructions that require this)

    • Ideally this would cause modreq literal to be emitted and would have compiler supports

    • This could probably go in System.Runtime.CompilerServices rather than S.R.CS.Intrinsics

  • Alignment enforces an alignment for a struct.

For SSE, there are 4 different register sizes: 64, 128, 256, and 512.

  • These are separate rather than a generic 'Vector` type so that the user can explicitly dictate which register size they are using (and therefore also control clearing of upper bits, etc)
  • These expose an IsSupported property so the user can determine whether the CPU supports that register size (for example, this can indicate support for https://software.intel.com/sites/landingpage/IntrinsicsGuide/#text=_mm256_cvtss&expand=1809).
  • These structs are currently generic and do not expose the underlying fields (which should not be accessed directly anyways)
  • It might be worthwhile seeing if we can enforce these to be "stack only". Ideally users would always guarantee they have a software fallback and create their types as such as well. That is, they would never declare Simd128<float> as a field, and instead would declare float x, y, z, w and would explicitly call Load and Store to get a Simd128<float>

The SSE static class exposes all the instructions supported by the SSE CPUID bit flag:

  • Methods which do not have a software implementation are marked as 'extern'

    • The runtime should throw a PNSE if IsSupported is false

    • This forces any consumers (such as AOT compilers) to understand the method

  • Most of the method names can be differentiated by overload

    • For some method names, we have to append 'Scalar', since the packed and scalar forms both take Simd128<float> and just treat the parameter differently.

  • Some of the names may not be ideal, I just chose something that made some sense

```C#
namespace System.Runtime.CompilerServices.Intrinsics
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class IntrinsicAttribute : Attribute
{
// Indicates that a method is an intrinsic
// Optional. Allow users to specify useful metadata such as the corresponding C/C++ intrinsic or hardware instruction
}

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public sealed class LiteralAttribute : Attribute
{
    // Indicates that a parameter is required to be literal.
    // Should be emitted as a `modreq literal`
}

[AttributeUsage(AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class AlignmentAttribute : Attribute
{
    // Indicates the required alignment of a type.

    public AlignmentAttribute(int value) { }
}

[Alignment(8)]
public struct Simd64<T>
{
    // This should be recognized by the JIT as a constant
    // This indicates whether the hardware supports holding values of T
    public static bool IsSupported { get; }
}

[Alignment(16)]
public struct Simd128<T>
{
    // This should be recognized by the JIT as a constant
    // This indicates whether the hardware supports holding values of T
    public static bool IsSupported { get; }
}

[Alignment(32)]
public struct Simd256<T>
{
    // This should be recognized by the JIT as a constant
    // This indicates whether the hardware supports holding values of T
    public static bool IsSupported { get; }
}

[Alignment(64)]
public struct Simd512<T>
{
    // This should be recognized by the JIT as a constant
    // This indicates whether the hardware supports holding values of T
    public static bool IsSupported { get; }
}

namespace x86
{
    public static unsafe class SSE
    {
        // This should be recognized by the JIT as a constant
        // This indicates whether the hardware supports this instruction set.
        public static bool IsSupported { get; }

        #region Data Transfer Instructions
        [Intrinsic] // MOVAPS, _mm_load_ps
        public static extern Simd128<float> LoadAligned(float* address);

        [Intrinsic] // MOVAPS, _mm_store_ps
        public static extern void StoreAligned(float* address, Simd128<float> value);

        [Intrinsic] // MOVUPS, _mm_loadu_ps
        public static extern Simd128<float> LoadUnaligned(float* address);

        [Intrinsic] // MOVUPS, _mm_storeu_ps
        public static extern void StoreUnaligned(float* address, Simd128<float> value);

        [Intrinsic] // MOVHPS, _mm_loadh_pi
        public static extern Simd128<float> LoadHigh(Simd128<float> value, float* address);

        [Intrinsic] // MOVHPS, _mm_storeh_pi
        public static extern void StoreHigh(float* address, Simd128<float> value);

        [Intrinsic] // MOVHLPS, _mm_movehl_ps
        public static extern Simd128<float> MoveHighLow(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MOVLPS, _mm_loadl_pi
        public static extern Simd128<float> LoadLow(Simd128<float> value, float* address);

        [Intrinsic] // MOVLPS, _mm_storel_pi
        public static extern void StoreLow(float* address, Simd128<float> value);

        [Intrinsic] // MOVLHPS, _mm_movelh_ps 
        public static extern Simd128<float> MoveLowHigh(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MOVMSKPS, _mm_movemask_ps
        public static extern int MoveMask(Simd128<float> value);

        [Intrinsic] // MOVSS, _mm_cvtss_f32
        public static extern float ToSingle(Simd128<float> value);

        [Intrinsic] // MOVSS, _mm256_cvtss_f32
        public static extern float ToSingle(Simd256<float> value);

        [Intrinsic] // MOVSS, _mm512_cvtss_f32
        public static extern float ToSingle(Simd512<float> value);

        [Intrinsic] // MOVSS, _mm_load_ss
        public static extern Simd128<float> Load(float* address);

        [Intrinsic] // MOVSS, _mm_move_ss
        public static extern Simd128<float> Move(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MOVSS, _mm_store_ss
        public static extern void Store(float* address, Simd128<float> value);
        #endregion

        #region Arithmetic Instructions
        [Intrinsic] // ADDPS, _mm_add_ps
        public static extern Simd128<float> Add(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // ADDSS, _mm_add_ss
        public static extern Simd128<float> AddScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // SUBPS, _mm_sub_ps
        public static extern Simd128<float> Subtract(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // SUBSS, _mm_sub_ss
        public static extern Simd128<float> SubtractScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MULPS, _mm_mul_ps
        public static extern Simd128<float> Multiply(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MULSS, _mm_mul_ss
        public static extern Simd128<float> MultiplyScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // DIVPS, _mm_div_ps
        public static extern Simd128<float> Divide(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // DIVSS, _mm_div_ss
        public static extern Simd128<float> DivideScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // RCPPS, _mm_rcp_ps
        public static extern Simd128<float> Reciprocal(Simd128<float> value);

        [Intrinsic] // RCPSS, _mm_rcp_ss
        public static extern Simd128<float> ReciprocalScalar(Simd128<float> value);

        [Intrinsic] // SQRTPS, _mm_sqrt_ps
        public static extern Simd128<float> Sqrt(Simd128<float> value);

        [Intrinsic] // SQRTSS, _mm_sqrt_ss
        public static extern Simd128<float> SqrtScalar(Simd128<float> value);

        [Intrinsic] // RSQRTPS, _mm_rsqrt_ps
        public static extern Simd128<float> ReciprocalSqrt(Simd128<float> value);

        [Intrinsic] // RSQRTSS, _mm_rsqrt_ss
        public static extern Simd128<float> ReciprocalSqrtScalar(Simd128<float> value);

        [Intrinsic] // MAXPS, _mm_max_ps
        public static extern Simd128<float> Max(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MAXSS, _mm_max_ss
        public static extern Simd128<float> MaxScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MINPS, _mm_min_ps
        public static extern Simd128<float> Min(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // MINSS, _mm_min_ss
        public static extern Simd128<float> MinScalar(Simd128<float> a, Simd128<float> b);
        #endregion

        #region Comparison Instructions
        [Intrinsic] // CMPPS, _mm_cmpeq_ps
        public static extern Simd128<float> CompareEqual(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpge_ps
        public static extern Simd128<float> CompareGreaterOrEqual(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpgt_ps
        public static extern Simd128<float> CompareGreater(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmple_ps
        public static extern Simd128<float> CompareLessOrEqual(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmplt_ps
        public static extern Simd128<float> CompareLess(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpneq_ps
        public static extern Simd128<float> CompareNotEqual(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpnge_ps
        public static extern Simd128<float> CompareNotGreaterOrEqual(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpngt_ps
        public static extern Simd128<float> CompareNotGreater(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpnle_ps
        public static extern Simd128<float> CompareNotLessOrEqual(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpnlt_ps
        public static extern Simd128<float> CompareNotLess(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpord_ps
        public static extern Simd128<float> CompareOrdered(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPPS, _mm_cmpunord_ps
        public static extern Simd128<float> CompareUnordered(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpeq_ss
        public static extern Simd128<float> CompareEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpge_ss
        public static extern Simd128<float> CompareGreaterOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpgt_ss
        public static extern Simd128<float> CompareGreaterScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmple_ss
        public static extern Simd128<float> CompareLessOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmplt_ss
        public static extern Simd128<float> CompareLessScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpneq_ss
        public static extern Simd128<float> CompareNotEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpnge_ss
        public static extern Simd128<float> CompareNotGreaterOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpngt_ss
        public static extern Simd128<float> CompareNotGreaterScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpnle_ss
        public static extern Simd128<float> CompareNotLessOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpnlt_ss
        public static extern Simd128<float> CompareNotLessScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpord_ss
        public static extern Simd128<float> CompareOrderedScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // CMPSS, _mm_cmpunord_ss
        public static extern Simd128<float> CompareUnorderedScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // COMISS, _mm_comieq_ss
        public static extern bool OrderedCompareEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // COMISS, _mm_comige_ss
        public static extern bool OrderedCompareGreaterOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // COMISS, _mm_comigt_ss
        public static extern bool OrderedCompareGreaterScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // COMISS, _mm_comile_ss
        public static extern bool OrderedCompareLessOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // COMISS, _mm_comilt_ss
        public static extern bool OrderedCompareLessScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // COMISS, _mm_comineq_ss
        public static extern bool OrderedCompareNotEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // UCOMISS, _mm_ucomieq_ss
        public static extern bool UnorderedCompareEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // UCOMISS, _mm_ucomige_ss
        public static extern bool UnorderedCompareGreaterOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // UCOMISS, _mm_ucomigt_ss
        public static extern bool UnorderedCompareGreaterScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // UCOMISS, _mm_ucomile_ss
        public static extern bool UnorderedCompareLessOrEqualScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // UCOMISS, _mm_ucomilt_ss
        public static extern bool UnorderedCompareLessScalar(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // UCOMISS, _mm_ucomineq_ss
        public static extern bool UnorderedCompareNotEqualScalar(Simd128<float> a, Simd128<float> b);
        #endregion

        #region Logical Instructions
        [Intrinsic] // ANDPS, _mm_and_ps
        public static extern Simd128<float> And(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // ANDNPS, _mm_andnot_ps
        public static extern Simd128<float> AndNot(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // ORPS, _mm_or_ps
        public static extern Simd128<float> Or(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // XORPS, _mm_xor_ps
        public static extern Simd128<float> XOr(Simd128<float> a, Simd128<float> b);
        #endregion

        #region Shuffle and Unpack Instructions
        [Intrinsic] //  SHUFPS, _mm_shuffle_ps
        public static extern Simd128<float> Shuffle(Simd128<float> a, Simd128<float> b, [Literal] byte imm8);

        [Intrinsic] // UNPCKHPS, _mm_unpackhi_ps
        public static extern Simd128<float> UnpackHigh(Simd128<float> a, Simd128<float> b);

        [Intrinsic] // UNPCKLPS, _mm_unpacklo_ps
        public static extern Simd128<float> UnpackLow(Simd128<float> a, Simd128<float> b);
        #endregion

        #region Conversion Instructions
        [Intrinsic] // CVTPI2PS, _mm_cvt_pi2ps, _mm_cvtpi32_ps
        public static extern Simd128<float> ToPackedSingle(Simd128<float> a, Simd64<int> b);

        [Intrinsic] // CVTSI2SS, _mm_cvt_si2ss, _mm_cvtsi32_ss
        public static extern Simd128<float> ToPackedSingle(Simd128<float> a, int b);

        [Intrinsic] // CVTSI2SS, _mm_cvtsi64_ss
        public static extern Simd128<float> ToPackedSingle(Simd128<float> a, long b);

        [Intrinsic] // CVTPS2PI, _mm_cvt_ps2pi, _mm_cvtps_pi32
        public static extern Simd64<int> ToPackedInt32(Simd128<float> value);

        [Intrinsic] // CVTTPS2PI, _mm_cvtt_ps2pi, _mm_cvttps_pi32
        public static extern Simd64<int> ToPackedInt32Truncated(Simd128<float> value);

        [Intrinsic] // CVTSS2SI, _mm_cvt_ss2si, _mm_cvtss_si32
        public static extern int ToInt32(Simd128<float> value);

        [Intrinsic] // CVTSS2SI, _mm_cvtss_si64
        public static extern long ToInt64(Simd128<float> value);

        [Intrinsic] // CVTTSS2SI, _mm_cvvt_ss2si, _mm_cvttss_si32
        public static extern int ToInt32Truncated(Simd128<float> value);

        [Intrinsic] // CVTTSS2SI, _mm_cvttss_si64
        public static extern long ToInt64Truncated(Simd128<float> value);
        #endregion

        #region MXCSR State Management Instructions
        [Intrinsic] // LDMXCSR, _mm_setcsr
        public static extern void SetControlStatus(uint a);

        [Intrinsic] // STMXCSR, _mm_getcsr
        public static extern uint GetControlStatus();
        #endregion

        #region 64-bit Integer Instructions
        [Intrinsic] // PAVGB, _mm_avg_pu8, _m_pavgb
        public static extern Simd64<byte> Average(Simd64<byte> a, Simd64<byte> b);

        [Intrinsic] // PAVGW, _mm_avg_pu16, _m_pavgw
        public static extern Simd64<ushort> Average(Simd64<ushort> a, Simd64<ushort> b);

        [Intrinsic] // PEXTRW, _mm_extract_pi16, _m_pextrw
        public static extern short Extract(Simd64<short> a, [Literal] byte imm8);

        [Intrinsic] // PINSRW, _mm_insert_pi16, _m_pinsrw
        public static extern Simd64<short> Insert(Simd64<short> a, short i, [Literal] byte imm8);

        [Intrinsic] // PMAXUB, _mm_max_pu8, _m_pmaxub
        public static extern Simd64<byte> Max(Simd64<byte> a, Simd64<byte> b);

        [Intrinsic] // PMAXSW, _mm_max_pu16, _m_pmaxuw
        public static extern Simd64<ushort> Max(Simd64<ushort> a, Simd64<ushort> b);

        [Intrinsic] // PMINUB, _mm_min_pu8, _m_pminub
        public static extern Simd64<byte> Min(Simd64<byte> a, Simd64<byte> b);

        [Intrinsic] // PMINSW, _mm_min_pu16, _m_pminuw
        public static extern Simd64<ushort> Min(Simd64<ushort> a, Simd64<ushort> b);

        [Intrinsic] // PMOVMSKB, _mm_movemask_pi8, _m_pmovmskb
        public static extern int MoveMask(Simd64<byte> value);

        [Intrinsic] // PMULHUWm _mm_mulhi_pu16, _m_pmulhuw
        public static extern Simd64<ushort> MultiplyHigh(Simd64<ushort> a, Simd64<ushort> b);

        [Intrinsic] // PSADBW, _mm_sad_pu8, _m_psadbw
        public static extern Simd64<ushort> AbsoluteSumOfDifferences(Simd64<byte> a, Simd64<byte> b);

        [Intrinsic]  // PSHUFW, _mm_shuffle_pi16, _m_pshufw
        public static extern Simd64<short> Shuffle(Simd64<short> value, [Literal] byte imm8);
        #endregion

        #region Cacheability Control, Prefetch, and Instruction Ordering Instructions
        [Intrinsic] // MASKMOVQ, _mm_maskmove_si64, _m_maskmovq
        public static extern void MaskMove(Simd64<byte> a, Simd64<byte> mask, byte* address);

        [Intrinsic] // MOVNTQ, _mm_stream_pi
        public static extern void Stream(long* address, Simd64<byte> value);

        [Intrinsic] // MOVNTPS, _mm_stream_ps
        public static extern void Stream(float* address, Simd128<float> value);

        [Intrinsic] // PREFETCHNTA, PREFETCH0, PREFETCH1, PREFETCH2, _mm_prefetch
        public static extern void Prefetch(byte* p, int i);

        [Intrinsic] // SFENCE, _mm_sfence
        public static extern void StoreFence();
        #endregion
    }
}

}
```

api-needs-work area-System.Runtime.CompilerServices

Most helpful comment

Intrinsic identifies a method as exposing an intrinsic

The Intrinsic attribute should be internal implementation detail. It should not be part of the public surface.

All 18 comments

FYI. @mellinoe, @russellhadley.

Just an exploration with some notes

Intrinsic identifies a method as exposing an intrinsic

The Intrinsic attribute should be internal implementation detail. It should not be part of the public surface.

That's HUUUGE for all performance critical code. I would like to compliment DotNet team for this decision.

I believe we need a minimum of 3 attributes:

Literal identifiers a parameter that is required to be a literal (there are a few instructions that require this)
Ideally this would cause modreq literal to be emitted and would have compiler supports
This could probably go in System.Runtime.CompilerServices rather than S.R.CS.Intrinsics

IMO it would be wrong to use attribute to enforce syntactic and semantic requirements in the programming language if there is a simple syntactic alternative (see https://github.com/dotnet/csharplang/issues/744 and discussion in https://github.com/dotnet/corefx/issues/16835). In any statically typed language it is a type system which is used in a first place to enforce type safety and what is more important it is used in a way which aligns well with language style what makes developers life easier.

Implementation of const parameter feature in Roslyn should be trivial and would improve C# expressiveness in a way consistent with current syntax. It would be a natural syntactic extension of accepted readonly ref parameters language feature to compile time constants.

Furthermore parameter required for several intrinsics is a compile time const integer. In C# it does not necessarily mean that it has to be literal expression as a constexpr provides a more flexible equivalent and is accepted in const declaration syntax.

From type safety perspective it would be very hard to prove that syntax enforcement via attribute is sound, correct and verifiable (it is not impossible but significantly harder as a prerequisite to a proof one would have to prove soundness of underlying attribute type system including syntactic quirks like LiteralAttribute). AFAIR all features of C# and CIL have been formally proven to be sound and verifiable For examples see:

IMO it would be wrong to use attribute to enforce syntactic and semantic requirements in the programming language if there is a simple syntactic alternative

To be clear, if the language did end up supporting constexpr it would likely end up emitted as an attribute as well. You need to be able to carry that metadata around in the post-compilation assembly so that consumers know any restrictions as well as being able to detect that the type/method/field supports constexpr. Carrying around that metadata is restricted to the ways exposed by the IL to do so which pretty much boils down to modopt, modreq, and general attributes.

'modreq' and modopt are special attributes that attach to a method parameter and become part of the method signature. modreq requires that the compiler understand the meaning of the attached 'keyword' or refuse to compile.

The C# compiler already uses all three of these attribute types to attach various metadata for features it provides (readonly ref uses modreq extensively).

It may additionally be worthwhile to note that constants in C# are really just IL literals. They exist for the compiler, purely in metadata, and are never directly processed by the JIT. Instead, any use by the compiler is directly inlined to the generated IL.

For constexpr, the compiler would likely do automatic folding (as per the feature) and emit literals inline as well (thereby dropping any method calls completely). So, a constexpr that returns a primitive type can be treated as literal.

In the end, if the language decide to support a modreq for this, it would end up being their decision on how it was implemented and how it integrates with other features. They would naturally take into account the cost and any future plans in related areas (such as constexpr). If they instead decided to not do this, there would likely need to be an analyzer to help enforce users are doing the right thing.

To be clear, if the language did end up supporting constexpr it would likely end up emitted as an attribute as well

Carrying around that metadata is restricted to the ways exposed by the IL to do so which pretty much boils down to modopt, modreq, and general attributes.

I think we are discussing two different problems:

  1. C# syntax used to support enforcement of passing immediate value which is required for some SIMD opcodes - this one is visible to users of the API,
  2. CIL syntax which is used to implement it - this one is an implementation detail which should be as effective as possible but is not that important for API users.

Ad. 1.
Both attribute usage and const parameter proposal were authored by me (issue dotnet/runtime#20510) but I am not married to any of them. I would like to see best and most intuitive and concise syntax for this feature. Let's see the consequences of either choice from user perspective by comparing it on examples:

```C#
public static Simd256 Shuffle(Simd256 value, [LiteralAttribute()] int mask);
public static Simd256 Shuffle(Simd256 value, const int mask);

Simd256 a, b;
int x = Foo();
// .... initialization code

b = Simd256.Shuffle(a, x); // compile error bcs x was declared with LiteralAttribute
b = Simd256.Shuffle(a, b, const x); // compile error bcs x is not compile time constant
```
I prefer second solution as it is clean and aligns well with C# syntaxt.

Ad. 2.

For me it is just secondary to C# syntax problem.

My primary point is that [LiteralAttribute] int mask could be supported today with very little in the way of the language (it essentially boils down to a compiler only change to understand the restrictions around the method).

While const int mask, on the other hand, requires significant more discussion, design, implementation, etc (as it becomes a feature of the language, rather than something simply understood by the compiler).

Additionally, you can do LiteralAttribute today and later change that to be const in the language if/when constexpr gets implemented by the language.

I would, personally, rather see this feature come to light without it being delayed by lengthy language design discussions and/or by being blocked by a language feature that is mostly orthogonal.

Additionally, you can do LiteralAttribute today and later change that to be const in the language if/when constexpr gets implemented by the language.

I would, personally, rather see this feature come to light without it being delayed by lengthy language design discussions and/or by being blocked by a language feature that is mostly orthogonal.

I agree - I would prefer to have it as fast as possible and if it is possible to get it faster without language syntax changes than this is the way to go.

Is it possible to explore x86 SIMD API which comprises much more than SSE or the title by intention limits discussion to SSE extensions only?

@4creators, I created this post mostly as just an exploration of what such a thing might look like and to help get discussions going (because I am actively interested in this).

Keeping in mind that this isn't an "official" proposal, It could definitely be expanded to any other intrinsic types (ARM NEON, x86 SSE2, x86 FMA, etc) to help explore any deficiencies or strong-points in the current exploration/etc. I feel like exploring how ARM and x86 intrinsics live SxS and what can or cannot be shared between the two will probably provide the most relevance.

OK I understand - will come up with some comments on it. I am personally interested in this feature as well :)

C# namespace x86 { public static unsafe class SSE { // This should be recognized by the JIT as a constant // This indicates whether the hardware supports this instruction set. public static bool IsSupported { get; }

I think that query for support of ISA extensions could be done either in RuntimeInformation class or by using new API from issue dotnet/runtime#22948 (let's call it here System.Diagnostics.Hardware) as this information could be useful not only for SIMD intrinsics but for Vector<T>

I think that query for support of ISA extensions could be done either...

I think, even if it was exposed through System.Diagnostics.Hardware, it should still be exposed directly on the respective intrinsic classes. That is, the information should be readily available where it is relevant (within the class that exposes the intrinsics tied to that flag).

My hope for this feature (and hopefully it matches the goals of CoreFX/CoreCLR) is that:

  • It is part of the lowest layers of CoreFX/CoreCLR and that it can be consumed by all other layers

    • Being able to write an implementation of System.Math and System.MathF or System.String functions that is fast, efficient, vectorized, and written in managed code would be great

  • Existing intrinsic functions (such as Vector<T>) can be rewritten using these APIs.

    • This will not only simplify the JIT/runtime overhead for these APIs, but will make it significantly easier to make updates/improvements as well (it boils down to writing managed code, rather than having to deal with JIT internals).

But how you can do System.Math using these APIs in a portable way? If you make call Math.Sqrt() the function SSE.SqrtScalar() how it could work on ARM?
The idea is to use a sort of "redirection" based on platform / architecture anyway?

@fanoI, in my mind, you would implement as follows:

C# public static double Sqrt(double x) { if (x86.SSE2.IsSupported) { // Do x86 SSE2 implementation } else if (Arm.Neon.IsSupported) { // Do Arm implementation } else { // Do Software Fallback implementation } }

For a JIT, it would treat IsSupported as a constant (as it does with typeof(T) and Vector<T>.IsHardwareAccelerated) and would end up dropping the code-paths for irrelevant platforms and for ISAs that aren't supported.

For an AOT, it would likely drop irrelevant platforms and compile all ISAs. It would also likely emit code so that the method calls into the optimal ISA for the current CPU (via a jump table, dynamic dispatch, etc).

Intel hardware intrinsic API proposal has been opened at dotnet/corefx#22940

I can expect x86.SSE3.IsSupported, x86.SSE4.IsSupported and x86.AVX.IsSupported too?
Mmh those "if" / "else if" chains risk to become really long soon :-)

In Cosmos we can create plugs in X# directly so I'm unsure if this will be useful for us really or not... well we can plug x86.SSE2.Sqrt and hope that all "System.Math" does not need to be plugged being not native code anymore from our point of view.

Going to close this since we have an "official" proposal now.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sahithreddyk picture sahithreddyk  Â·  3Comments

yahorsi picture yahorsi  Â·  3Comments

iCodeWebApps picture iCodeWebApps  Â·  3Comments

jzabroski picture jzabroski  Â·  3Comments

chunseoklee picture chunseoklee  Â·  3Comments